mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-05-22 21:17:48 +00:00
Compare commits
108 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8cf76b20c7 | ||
|
|
44c1633952 | ||
|
|
f83ad5af27 | ||
|
|
a5afd011a1 | ||
|
|
c88891da5b | ||
|
|
b1af0c800b | ||
|
|
146d2c8b66 | ||
|
|
a18e193611 | ||
|
|
6ae0bc36cc | ||
|
|
b1410c34e0 | ||
|
|
070991d7ef | ||
|
|
874f59f0ff | ||
|
|
fa547b5aac | ||
|
|
d27580c016 | ||
|
|
2093ef1ea2 | ||
|
|
75540a588d | ||
|
|
fe51b4c56a | ||
|
|
c7c42f6983 | ||
|
|
68df1b1281 | ||
|
|
00efcfaae5 | ||
|
|
c6b194a073 | ||
|
|
8ac44dfbb3 | ||
|
|
ea2ab9edc0 | ||
|
|
9d27c705f6 | ||
|
|
ebdd9755ba | ||
|
|
fa2a8c5084 | ||
|
|
ac2d73b57c | ||
|
|
93902e6364 | ||
|
|
42163731b3 | ||
|
|
01ce04fe9b | ||
|
|
5d29363764 | ||
|
|
bfe8bc9b73 | ||
|
|
a6f6235dd0 | ||
|
|
ee6bf011e9 | ||
|
|
bea8a7f984 | ||
|
|
2edca5dacb | ||
|
|
69ac73c556 | ||
|
|
89556ecd66 | ||
|
|
462f91f070 | ||
|
|
d29a77b15f | ||
|
|
c31fe521c4 | ||
|
|
8f6f542e9c | ||
|
|
69d9e7d405 | ||
|
|
5e6748a88a | ||
|
|
8bc70f9c87 | ||
|
|
357d226987 | ||
|
|
6b1f7138c6 | ||
|
|
55219dc51b | ||
|
|
0eb3a9a43c | ||
|
|
4d7136b7a7 | ||
|
|
b7d9e1d85d | ||
|
|
a714058328 | ||
|
|
02ff4a1bac | ||
|
|
7833e1337e | ||
|
|
18e0e64ca0 | ||
|
|
80a3d88359 | ||
|
|
5d72028872 | ||
|
|
e89776848c | ||
|
|
70bc78e765 | ||
|
|
4fceae7733 | ||
|
|
23b7dd4abf | ||
|
|
0216bbd1d9 | ||
|
|
15a4fa45e0 | ||
|
|
f2d9ffd5f6 | ||
|
|
b0ba7c8671 | ||
|
|
e5fa25892e | ||
|
|
ae5fe24556 | ||
|
|
b9ef0aa27a | ||
|
|
f30625acf0 | ||
|
|
66d70305a0 | ||
|
|
9108b7ebec | ||
|
|
9454049220 | ||
|
|
2ce2e01803 | ||
|
|
ab25d6c5ab | ||
|
|
1b0dc0cfb0 | ||
|
|
33e484a847 | ||
|
|
67bebc0491 | ||
|
|
9186a74ae3 | ||
|
|
67d9a9819e | ||
|
|
16b88a29db | ||
|
|
9783793514 | ||
|
|
93de398a09 | ||
|
|
23e97a4284 | ||
|
|
9bb906551e | ||
|
|
b1ad82cef9 | ||
|
|
09c9f2930c | ||
|
|
74efbd7a61 | ||
|
|
1b1e7d9bce | ||
|
|
dc98fc4839 | ||
|
|
a815f97c7e | ||
|
|
8261cf2811 | ||
|
|
f2238935b5 | ||
|
|
5f12f76ada | ||
|
|
5a9464697f | ||
|
|
f44601a82b | ||
|
|
3d3de4a884 | ||
|
|
c02ad56b6d | ||
|
|
ea7c5ccd58 | ||
|
|
7ec3eae72b | ||
|
|
edf7d18308 | ||
|
|
6bf4a4e91d | ||
|
|
5fd966f042 | ||
|
|
e275f20055 | ||
|
|
ff624a34d8 | ||
|
|
c98050224c | ||
|
|
5bee373091 | ||
|
|
db4ab34ddf | ||
|
|
ef138fb5aa |
28
.github/workflows/ci.yml
vendored
28
.github/workflows/ci.yml
vendored
@@ -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
1
.gitignore
vendored
@@ -62,6 +62,7 @@ buck-out/
|
||||
|
||||
# fastlane
|
||||
#
|
||||
.bundle/
|
||||
**/fastlane/report.xml
|
||||
**/fastlane/Preview.html
|
||||
**/fastlane/test_output
|
||||
|
||||
16
Gemfile
Normal file
16
Gemfile
Normal 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
331
Gemfile.lock
Normal 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
|
||||
@@ -30,9 +30,12 @@ import android.view.KeyEvent;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.oney.WebRTCModule.WebRTCModuleOptions;
|
||||
|
||||
import org.jitsi.meet.sdk.JitsiMeet;
|
||||
import org.jitsi.meet.sdk.JitsiMeetActivity;
|
||||
import org.jitsi.meet.sdk.JitsiMeetConferenceOptions;
|
||||
import org.webrtc.Logging;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.URL;
|
||||
@@ -79,6 +82,10 @@ public class MainActivity extends JitsiMeetActivity {
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
JitsiMeet.showSplashScreen(this);
|
||||
|
||||
WebRTCModuleOptions options = WebRTCModuleOptions.getInstance();
|
||||
options.loggingSeverity = Logging.Severity.LS_ERROR;
|
||||
|
||||
super.onCreate(null);
|
||||
}
|
||||
|
||||
|
||||
@@ -89,9 +89,11 @@ dependencies {
|
||||
implementation project(':react-native-splash-screen')
|
||||
implementation project(':react-native-svg')
|
||||
implementation project(':react-native-video')
|
||||
implementation project(':react-native-webrtc')
|
||||
implementation project(':react-native-webview')
|
||||
|
||||
// Use `api` here so consumers can use WebRTCModuleOptions.
|
||||
api project(':react-native-webrtc')
|
||||
|
||||
testImplementation 'junit:junit:4.12'
|
||||
}
|
||||
|
||||
|
||||
@@ -80,7 +80,9 @@ public class BroadcastAction {
|
||||
SET_CLOSED_CAPTIONS_ENABLED("org.jitsi.meet.SET_CLOSED_CAPTIONS_ENABLED"),
|
||||
TOGGLE_CAMERA("org.jitsi.meet.TOGGLE_CAMERA"),
|
||||
SHOW_NOTIFICATION("org.jitsi.meet.SHOW_NOTIFICATION"),
|
||||
HIDE_NOTIFICATION("org.jitsi.meet.HIDE_NOTIFICATION");
|
||||
HIDE_NOTIFICATION("org.jitsi.meet.HIDE_NOTIFICATION"),
|
||||
START_RECORDING("org.jitsi.meet.START_RECORDING"),
|
||||
STOP_RECORDING("org.jitsi.meet.STOP_RECORDING");
|
||||
|
||||
private final String action;
|
||||
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
package org.jitsi.meet.sdk;
|
||||
|
||||
import android.content.Intent;
|
||||
|
||||
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import android.os.Bundle;
|
||||
|
||||
public class BroadcastIntentHelper {
|
||||
public static Intent buildSetAudioMutedIntent(boolean muted) {
|
||||
Intent intent = new Intent(BroadcastAction.Type.SET_AUDIO_MUTED.getAction());
|
||||
intent.putExtra("muted", muted);
|
||||
|
||||
return intent;
|
||||
}
|
||||
|
||||
@@ -22,18 +19,21 @@ public class BroadcastIntentHelper {
|
||||
Intent intent = new Intent(BroadcastAction.Type.SEND_ENDPOINT_TEXT_MESSAGE.getAction());
|
||||
intent.putExtra("to", to);
|
||||
intent.putExtra("message", message);
|
||||
|
||||
return intent;
|
||||
}
|
||||
|
||||
public static Intent buildToggleScreenShareIntent(boolean enabled) {
|
||||
Intent intent = new Intent(BroadcastAction.Type.TOGGLE_SCREEN_SHARE.getAction());
|
||||
intent.putExtra("enabled", enabled);
|
||||
|
||||
return intent;
|
||||
}
|
||||
|
||||
public static Intent buildOpenChatIntent(String participantId) {
|
||||
Intent intent = new Intent(BroadcastAction.Type.OPEN_CHAT.getAction());
|
||||
intent.putExtra("to", participantId);
|
||||
|
||||
return intent;
|
||||
}
|
||||
|
||||
@@ -45,24 +45,28 @@ public class BroadcastIntentHelper {
|
||||
Intent intent = new Intent(BroadcastAction.Type.SEND_CHAT_MESSAGE.getAction());
|
||||
intent.putExtra("to", participantId);
|
||||
intent.putExtra("message", message);
|
||||
|
||||
return intent;
|
||||
}
|
||||
|
||||
public static Intent buildSetVideoMutedIntent(boolean muted) {
|
||||
Intent intent = new Intent(BroadcastAction.Type.SET_VIDEO_MUTED.getAction());
|
||||
intent.putExtra("muted", muted);
|
||||
|
||||
return intent;
|
||||
}
|
||||
|
||||
public static Intent buildSetClosedCaptionsEnabledIntent(boolean enabled) {
|
||||
Intent intent = new Intent(BroadcastAction.Type.SET_CLOSED_CAPTIONS_ENABLED.getAction());
|
||||
intent.putExtra("enabled", enabled);
|
||||
|
||||
return intent;
|
||||
}
|
||||
|
||||
public static Intent buildRetrieveParticipantsInfo(String requestId) {
|
||||
Intent intent = new Intent(BroadcastAction.Type.RETRIEVE_PARTICIPANTS_INFO.getAction());
|
||||
intent.putExtra("requestId", requestId);
|
||||
|
||||
return intent;
|
||||
}
|
||||
|
||||
@@ -78,12 +82,61 @@ public class BroadcastIntentHelper {
|
||||
intent.putExtra("timeout", timeout);
|
||||
intent.putExtra("title", title);
|
||||
intent.putExtra("uid", uid);
|
||||
|
||||
return intent;
|
||||
}
|
||||
|
||||
public static Intent buildHideNotificationIntent(String uid) {
|
||||
Intent intent = new Intent(BroadcastAction.Type.HIDE_NOTIFICATION.getAction());
|
||||
intent.putExtra("uid", uid);
|
||||
|
||||
return intent;
|
||||
}
|
||||
|
||||
public enum RecordingMode {
|
||||
FILE("file"),
|
||||
STREAM("stream");
|
||||
|
||||
private final String mode;
|
||||
|
||||
RecordingMode(String mode) {
|
||||
this.mode = mode;
|
||||
}
|
||||
|
||||
public String getMode() {
|
||||
return mode;
|
||||
}
|
||||
}
|
||||
|
||||
public static Intent buildStartRecordingIntent(
|
||||
RecordingMode mode,
|
||||
String dropboxToken,
|
||||
boolean shouldShare,
|
||||
String rtmpStreamKey,
|
||||
String rtmpBroadcastID,
|
||||
String youtubeStreamKey,
|
||||
String youtubeBroadcastID,
|
||||
Bundle extraMetadata,
|
||||
boolean transcription) {
|
||||
Intent intent = new Intent(BroadcastAction.Type.START_RECORDING.getAction());
|
||||
intent.putExtra("mode", mode.getMode());
|
||||
intent.putExtra("dropboxToken", dropboxToken);
|
||||
intent.putExtra("shouldShare", shouldShare);
|
||||
intent.putExtra("rtmpStreamKey", rtmpStreamKey);
|
||||
intent.putExtra("rtmpBroadcastID", rtmpBroadcastID);
|
||||
intent.putExtra("youtubeStreamKey", youtubeStreamKey);
|
||||
intent.putExtra("youtubeBroadcastID", youtubeBroadcastID);
|
||||
intent.putExtra("extraMetadata", extraMetadata);
|
||||
intent.putExtra("transcription", transcription);
|
||||
|
||||
return intent;
|
||||
}
|
||||
|
||||
public static Intent buildStopRecordingIntent(RecordingMode mode, boolean transcription) {
|
||||
Intent intent = new Intent(BroadcastAction.Type.STOP_RECORDING.getAction());
|
||||
intent.putExtra("mode", mode.getMode());
|
||||
intent.putExtra("transcription", transcription);
|
||||
|
||||
return intent;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,6 +99,8 @@ class ExternalAPIModule extends ReactContextBaseJavaModule {
|
||||
constants.put("TOGGLE_CAMERA", BroadcastAction.Type.TOGGLE_CAMERA.getAction());
|
||||
constants.put("SHOW_NOTIFICATION", BroadcastAction.Type.SHOW_NOTIFICATION.getAction());
|
||||
constants.put("HIDE_NOTIFICATION", BroadcastAction.Type.HIDE_NOTIFICATION.getAction());
|
||||
constants.put("START_RECORDING", BroadcastAction.Type.START_RECORDING.getAction());
|
||||
constants.put("STOP_RECORDING", BroadcastAction.Type.STOP_RECORDING.getAction());
|
||||
|
||||
return constants;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package org.jitsi.meet.sdk;
|
||||
|
||||
/*
|
||||
* Copyright 2017 The WebRTC project authors. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by a BSD-style license
|
||||
* that can be found in the LICENSE file in the root of the source
|
||||
* tree. An additional intellectual property rights grant can be found
|
||||
* in the file PATENTS. All contributing project authors may
|
||||
* be found in the AUTHORS file in the root of the source tree.
|
||||
*/
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.oney.WebRTCModule.webrtcutils.SoftwareVideoDecoderFactoryProxy;
|
||||
|
||||
import org.webrtc.EglBase;
|
||||
import org.webrtc.HardwareVideoDecoderFactory;
|
||||
import org.webrtc.PlatformSoftwareVideoDecoderFactory;
|
||||
import org.webrtc.VideoCodecInfo;
|
||||
import org.webrtc.VideoDecoder;
|
||||
import org.webrtc.VideoDecoderFactory;
|
||||
import org.webrtc.VideoDecoderFallback;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashSet;
|
||||
|
||||
/**
|
||||
* Custom decoder factory which uses HW decoders and falls back to SW.
|
||||
*/
|
||||
public class JitsiVideoDecoderFactory implements VideoDecoderFactory {
|
||||
private final VideoDecoderFactory hardwareVideoDecoderFactory;
|
||||
private final VideoDecoderFactory softwareVideoDecoderFactory = new SoftwareVideoDecoderFactoryProxy();
|
||||
private final @Nullable VideoDecoderFactory platformSoftwareVideoDecoderFactory;
|
||||
|
||||
/**
|
||||
* Create decoder factory using default hardware decoder factory.
|
||||
*/
|
||||
public JitsiVideoDecoderFactory(@Nullable EglBase.Context eglContext) {
|
||||
this.hardwareVideoDecoderFactory = new HardwareVideoDecoderFactory(eglContext);
|
||||
this.platformSoftwareVideoDecoderFactory = new PlatformSoftwareVideoDecoderFactory(eglContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create decoder factory using explicit hardware decoder factory.
|
||||
*/
|
||||
JitsiVideoDecoderFactory(VideoDecoderFactory hardwareVideoDecoderFactory) {
|
||||
this.hardwareVideoDecoderFactory = hardwareVideoDecoderFactory;
|
||||
this.platformSoftwareVideoDecoderFactory = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable VideoDecoder createDecoder(VideoCodecInfo codecType) {
|
||||
VideoDecoder softwareDecoder = softwareVideoDecoderFactory.createDecoder(codecType);
|
||||
final VideoDecoder hardwareDecoder = hardwareVideoDecoderFactory.createDecoder(codecType);
|
||||
if (softwareDecoder == null && platformSoftwareVideoDecoderFactory != null) {
|
||||
softwareDecoder = platformSoftwareVideoDecoderFactory.createDecoder(codecType);
|
||||
}
|
||||
if (hardwareDecoder != null && softwareDecoder != null) {
|
||||
// Both hardware and software supported, wrap it in a software fallback
|
||||
return new VideoDecoderFallback(
|
||||
/* fallback= */ softwareDecoder, /* primary= */ hardwareDecoder);
|
||||
}
|
||||
return hardwareDecoder != null ? hardwareDecoder : softwareDecoder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VideoCodecInfo[] getSupportedCodecs() {
|
||||
LinkedHashSet<VideoCodecInfo> supportedCodecInfos = new LinkedHashSet<>();
|
||||
|
||||
supportedCodecInfos.addAll(Arrays.asList(softwareVideoDecoderFactory.getSupportedCodecs()));
|
||||
supportedCodecInfos.addAll(Arrays.asList(hardwareVideoDecoderFactory.getSupportedCodecs()));
|
||||
if (platformSoftwareVideoDecoderFactory != null) {
|
||||
supportedCodecInfos.addAll(
|
||||
Arrays.asList(platformSoftwareVideoDecoderFactory.getSupportedCodecs()));
|
||||
}
|
||||
|
||||
return supportedCodecInfos.toArray(new VideoCodecInfo[supportedCodecInfos.size()]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.jitsi.meet.sdk;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.oney.WebRTCModule.webrtcutils.H264AndSoftwareVideoEncoderFactory;
|
||||
|
||||
import org.webrtc.EglBase;
|
||||
|
||||
/**
|
||||
* Custom encoder factory which uses HW for H.264 and SW for everything else.
|
||||
*/
|
||||
public class JitsiVideoEncoderFactory extends H264AndSoftwareVideoEncoderFactory {
|
||||
public JitsiVideoEncoderFactory(@Nullable EglBase.Context eglContext) {
|
||||
super(eglContext);
|
||||
}
|
||||
}
|
||||
@@ -16,8 +16,8 @@
|
||||
|
||||
package org.jitsi.meet.sdk;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
@@ -32,12 +32,10 @@ import com.facebook.react.modules.core.DeviceEventManagerModule;
|
||||
import com.facebook.react.uimanager.ViewManager;
|
||||
import com.oney.WebRTCModule.EglUtils;
|
||||
import com.oney.WebRTCModule.WebRTCModuleOptions;
|
||||
import com.oney.WebRTCModule.webrtcutils.H264AndSoftwareVideoDecoderFactory;
|
||||
import com.oney.WebRTCModule.webrtcutils.H264AndSoftwareVideoEncoderFactory;
|
||||
|
||||
import org.devio.rn.splashscreen.SplashScreenModule;
|
||||
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
|
||||
import org.webrtc.EglBase;
|
||||
import org.webrtc.Logging;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.util.ArrayList;
|
||||
@@ -126,31 +124,31 @@ class ReactInstanceManagerHolder {
|
||||
// AmplitudeReactNativePackage
|
||||
try {
|
||||
Class<?> amplitudePackageClass = Class.forName("com.amplitude.reactnative.AmplitudeReactNativePackage");
|
||||
Constructor constructor = amplitudePackageClass.getConstructor();
|
||||
Constructor<?> constructor = amplitudePackageClass.getConstructor();
|
||||
packages.add((ReactPackage)constructor.newInstance());
|
||||
} catch (Exception e) {
|
||||
// Ignore any error, the module is not compiled when LIBRE_BUILD is enabled.
|
||||
Log.d(TAG, "Not loading AmplitudeReactNativePackage");
|
||||
JitsiMeetLogger.d(TAG, "Not loading AmplitudeReactNativePackage");
|
||||
}
|
||||
|
||||
// GiphyReactNativeSdkPackage
|
||||
try {
|
||||
Class<?> giphyPackageClass = Class.forName("com.giphyreactnativesdk.GiphyReactNativeSdkPackage");
|
||||
Constructor constructor = giphyPackageClass.getConstructor();
|
||||
Constructor<?> constructor = giphyPackageClass.getConstructor();
|
||||
packages.add((ReactPackage)constructor.newInstance());
|
||||
} catch (Exception e) {
|
||||
// Ignore any error, the module is not compiled when LIBRE_BUILD is enabled.
|
||||
Log.d(TAG, "Not loading GiphyReactNativeSdkPackage");
|
||||
JitsiMeetLogger.d(TAG, "Not loading GiphyReactNativeSdkPackage");
|
||||
}
|
||||
|
||||
// RNGoogleSignInPackage
|
||||
try {
|
||||
Class<?> googlePackageClass = Class.forName("com.reactnativegooglesignin.RNGoogleSigninPackage");
|
||||
Constructor constructor = googlePackageClass.getConstructor();
|
||||
Constructor<?> constructor = googlePackageClass.getConstructor();
|
||||
packages.add((ReactPackage)constructor.newInstance());
|
||||
} catch (Exception e) {
|
||||
// Ignore any error, the module is not compiled when LIBRE_BUILD is enabled.
|
||||
Log.d(TAG, "Not loading RNGoogleSignInPackage");
|
||||
JitsiMeetLogger.d(TAG, "Not loading RNGoogleSignInPackage");
|
||||
}
|
||||
|
||||
return packages;
|
||||
@@ -169,7 +167,7 @@ class ReactInstanceManagerHolder {
|
||||
= ReactInstanceManagerHolder.getReactInstanceManager();
|
||||
|
||||
if (reactInstanceManager != null) {
|
||||
ReactContext reactContext
|
||||
@SuppressLint("VisibleForTests") ReactContext reactContext
|
||||
= reactInstanceManager.getCurrentReactContext();
|
||||
|
||||
if (reactContext != null) {
|
||||
@@ -192,7 +190,7 @@ class ReactInstanceManagerHolder {
|
||||
*/
|
||||
static <T extends NativeModule> T getNativeModule(
|
||||
Class<T> nativeModuleClass) {
|
||||
ReactContext reactContext
|
||||
@SuppressLint("VisibleForTests") ReactContext reactContext
|
||||
= reactInstanceManager != null
|
||||
? reactInstanceManager.getCurrentReactContext() : null;
|
||||
|
||||
@@ -219,15 +217,18 @@ class ReactInstanceManagerHolder {
|
||||
|
||||
// Initialize the WebRTC module options.
|
||||
WebRTCModuleOptions options = WebRTCModuleOptions.getInstance();
|
||||
|
||||
EglBase.Context eglContext = EglUtils.getRootEglBaseContext();
|
||||
|
||||
options.videoDecoderFactory = new H264AndSoftwareVideoDecoderFactory(eglContext);
|
||||
options.videoEncoderFactory = new H264AndSoftwareVideoEncoderFactory(eglContext);
|
||||
options.enableMediaProjectionService = true;
|
||||
// options.loggingSeverity = Logging.Severity.LS_INFO;
|
||||
if (options.videoDecoderFactory == null || options.videoEncoderFactory == null) {
|
||||
EglBase.Context eglContext = EglUtils.getRootEglBaseContext();
|
||||
if (options.videoDecoderFactory == null) {
|
||||
options.videoDecoderFactory = new JitsiVideoDecoderFactory(eglContext);
|
||||
}
|
||||
if (options.videoEncoderFactory == null) {
|
||||
options.videoEncoderFactory = new JitsiVideoEncoderFactory(eglContext);
|
||||
}
|
||||
}
|
||||
|
||||
Log.d(TAG, "initializing RN with Activity");
|
||||
JitsiMeetLogger.d(TAG, "initializing RN");
|
||||
|
||||
reactInstanceManager
|
||||
= ReactInstanceManager.builder()
|
||||
|
||||
@@ -393,6 +393,9 @@ var config = {
|
||||
// // showPrejoinWarning: true,
|
||||
// // If true, the notification for recording start will display a link to download the cloud recording.
|
||||
// // showRecordingLink: true,
|
||||
// // If true, mutes audio and video when a recording begins and displays a dialog
|
||||
// // explaining the effect of unmuting.
|
||||
// // requireConsent: true,
|
||||
// },
|
||||
|
||||
// recordingService: {
|
||||
@@ -600,6 +603,7 @@ var config = {
|
||||
// short: 2500,
|
||||
// medium: 5000,
|
||||
// long: 10000,
|
||||
// extraLong: 60000,
|
||||
// },
|
||||
|
||||
// // Options for the recording limit notification.
|
||||
@@ -1857,6 +1861,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
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
flex-grow: 1;
|
||||
|
||||
.meetings-list-empty {
|
||||
text-align: center;
|
||||
|
||||
@@ -75,9 +75,12 @@ $welcomePageHeaderBackground: linear-gradient(0deg, rgba(0, 0, 0, 0.2), rgba(0,
|
||||
$welcomePageHeaderBackgroundPosition: center;
|
||||
$welcomePageHeaderBackgroundRepeat: none;
|
||||
$welcomePageHeaderBackgroundSize: cover;
|
||||
$welcomePageHeaderPaddingBottom: 15px;
|
||||
$welcomePageHeaderPadding: 1rem;
|
||||
$welcomePageHeaderTitleMaxWidth: initial;
|
||||
$welcomePageHeaderTextAlign: center;
|
||||
$welcomePageButtonBg: #0074E0;
|
||||
$welcomePageButtonHoverBg: #4687ED;
|
||||
$welcomePageButtonFocusOutline: #00225A;
|
||||
|
||||
$welcomePageHeaderContainerMarginTop: 104px;
|
||||
$welcomePageHeaderContainerDisplay: flex;
|
||||
|
||||
@@ -18,7 +18,7 @@ body.welcome-page {
|
||||
background-position: $welcomePageHeaderBackgroundPosition;
|
||||
background-repeat: $welcomePageHeaderBackgroundRepeat;
|
||||
background-size: $welcomePageHeaderBackgroundSize;
|
||||
padding-bottom: $welcomePageHeaderPaddingBottom;
|
||||
padding: $welcomePageHeaderPadding;
|
||||
background-color: #131519;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
@@ -219,14 +219,18 @@ body.welcome-page {
|
||||
.welcome-page-button {
|
||||
border: 0;
|
||||
font-size: 14px;
|
||||
background: #0074E0;
|
||||
background: $welcomePageButtonBg;
|
||||
border-radius: 3px;
|
||||
color: #FFFFFF;
|
||||
cursor: pointer;
|
||||
padding: 16px 20px;
|
||||
|
||||
transition: all 0.2s;
|
||||
&:focus-within {
|
||||
outline: auto 2px #022e61;
|
||||
outline: auto 2px $welcomePageButtonFocusOutline;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $welcomePageButtonHoverBg;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,8 +268,7 @@ body.welcome-page {
|
||||
|
||||
&.without-content {
|
||||
.welcome-card {
|
||||
min-width: 500px;
|
||||
max-width: 580px;
|
||||
max-width: 100dvw;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
136
ios/Podfile.lock
136
ios/Podfile.lock
@@ -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
|
||||
|
||||
|
||||
@@ -157,7 +157,6 @@
|
||||
4EB0603B260E09D000F524C5 /* SampleUploader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SampleUploader.swift; sourceTree = "<group>"; };
|
||||
4EC49B8625BED71300E76218 /* ReplayKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ReplayKit.framework; path = System/Library/Frameworks/ReplayKit.framework; sourceTree = SDKROOT; };
|
||||
5C1BE20ECD5DEEB48FED90B5 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
||||
6132EF172BDFF13200BBE14D /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = ../PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
||||
756FCE06C08D9B947653C98A /* Pods-JitsiMeet.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JitsiMeet.debug.xcconfig"; path = "Target Support Files/Pods-JitsiMeet/Pods-JitsiMeet.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
B3B083EB1D4955FF0069CEE7 /* app.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = app.entitlements; sourceTree = "<group>"; };
|
||||
D6152FF9E9F7B0E86F70A21D /* libPods-JitsiMeet.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-JitsiMeet.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
@@ -302,7 +301,6 @@
|
||||
0BEA5C351F7B8F73000D0AB4 /* WatchKit extension */,
|
||||
4EB06025260E026600F524C5 /* JitsiMeetBroadcast Extension */,
|
||||
CDD71F5E1157E9F283DF92A8 /* Pods */,
|
||||
6132EF172BDFF13200BBE14D /* PrivacyInfo.xcprivacy */,
|
||||
5C1BE20ECD5DEEB48FED90B5 /* PrivacyInfo.xcprivacy */,
|
||||
);
|
||||
indentWidth = 2;
|
||||
|
||||
@@ -145,7 +145,6 @@
|
||||
4ED4FFF12721B9B90074E620 /* JitsiAudioSession.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JitsiAudioSession.h; sourceTree = "<group>"; };
|
||||
4ED4FFF22721B9B90074E620 /* JitsiAudioSession.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = JitsiAudioSession.m; sourceTree = "<group>"; };
|
||||
4ED4FFF52721BAE10074E620 /* JitsiAudioSession+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "JitsiAudioSession+Private.h"; sourceTree = "<group>"; };
|
||||
6132EF172BDFF13200BBE14D /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = ../PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
||||
86389F55993FAAF6AEB3FA3E /* Pods-JitsiMeetSDKLite.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JitsiMeetSDKLite.release.xcconfig"; path = "../Pods/Target Support Files/Pods-JitsiMeetSDKLite/Pods-JitsiMeetSDKLite.release.xcconfig"; sourceTree = "<group>"; };
|
||||
891FE43DAD30BC8976683100 /* Pods-JitsiMeetSDK.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JitsiMeetSDK.release.xcconfig"; path = "../Pods/Target Support Files/Pods-JitsiMeetSDK/Pods-JitsiMeetSDK.release.xcconfig"; sourceTree = "<group>"; };
|
||||
8F48C340DE0D91D1012976C5 /* Pods-JitsiMeetSDKLite.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JitsiMeetSDKLite.debug.xcconfig"; path = "../Pods/Target Support Files/Pods-JitsiMeetSDKLite/Pods-JitsiMeetSDKLite.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
@@ -231,7 +230,6 @@
|
||||
0BD906E61EC0C00300C8C18E /* Products */,
|
||||
0BCA49681EC4BBE500B793EE /* Resources */,
|
||||
0BD906E71EC0C00300C8C18E /* src */,
|
||||
6132EF172BDFF13200BBE14D /* PrivacyInfo.xcprivacy */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
|
||||
@@ -33,5 +33,7 @@ static NSString * const sendEventNotificationName = @"org.jitsi.meet.SendEvent";
|
||||
- (void)toggleCamera;
|
||||
- (void)showNotification:(NSString*)appearance :(NSString*)description :(NSString*)timeout :(NSString*)title :(NSString*)uid;
|
||||
- (void)hideNotification:(NSString*)uid;
|
||||
- (void)startRecording:(NSString*)mode :(NSString*)dropboxToken :(BOOL)shouldShare :(NSString*)rtmpStreamKey :(NSString*)rtmpBroadcastID :(NSString*)youtubeStreamKey :(NSString*)youtubeBroadcastID :(NSDictionary*)extraMetadata :(BOOL)transcription;
|
||||
- (void)stopRecording:(NSString*)mode :(BOOL)transcription;
|
||||
|
||||
@end
|
||||
|
||||
@@ -30,13 +30,15 @@ static NSString * const setClosedCaptionsEnabledAction = @"org.jitsi.meet.SET_CL
|
||||
static NSString * const toggleCameraAction = @"org.jitsi.meet.TOGGLE_CAMERA";
|
||||
static NSString * const showNotificationAction = @"org.jitsi.meet.SHOW_NOTIFICATION";
|
||||
static NSString * const hideNotificationAction = @"org.jitsi.meet.HIDE_NOTIFICATION";
|
||||
static NSString * const startRecordingAction = @"org.jitsi.meet.START_RECORDING";
|
||||
static NSString * const stopRecordingAction = @"org.jitsi.meet.STOP_RECORDING";
|
||||
|
||||
@implementation ExternalAPI
|
||||
|
||||
static NSMapTable<NSString*, void (^)(NSArray* participantsInfo)> *participantInfoCompletionHandlers;
|
||||
|
||||
__attribute__((constructor))
|
||||
static void initializeViewsMap() {
|
||||
static void initializeViewsMap(void) {
|
||||
participantInfoCompletionHandlers = [NSMapTable strongToStrongObjectsMapTable];
|
||||
}
|
||||
|
||||
@@ -56,7 +58,9 @@ RCT_EXPORT_MODULE();
|
||||
@"SET_CLOSED_CAPTIONS_ENABLED": setClosedCaptionsEnabledAction,
|
||||
@"TOGGLE_CAMERA": toggleCameraAction,
|
||||
@"SHOW_NOTIFICATION": showNotificationAction,
|
||||
@"HIDE_NOTIFICATION": hideNotificationAction
|
||||
@"HIDE_NOTIFICATION": hideNotificationAction,
|
||||
@"START_RECORDING": startRecordingAction,
|
||||
@"STOP_RECORDING": stopRecordingAction
|
||||
};
|
||||
};
|
||||
|
||||
@@ -84,7 +88,9 @@ RCT_EXPORT_MODULE();
|
||||
setClosedCaptionsEnabledAction,
|
||||
toggleCameraAction,
|
||||
showNotificationAction,
|
||||
hideNotificationAction
|
||||
hideNotificationAction,
|
||||
startRecordingAction,
|
||||
stopRecordingAction
|
||||
];
|
||||
}
|
||||
|
||||
@@ -186,7 +192,7 @@ RCT_EXPORT_METHOD(sendEvent:(NSString *)name
|
||||
[self sendEventWithName:toggleCameraAction body:nil];
|
||||
}
|
||||
|
||||
- (void)showNotification:(NSString *)appearance :(NSString *)description :(NSString *)timeout :(NSString *)title :(NSString *)uid {
|
||||
- (void)showNotification:(NSString*)appearance :(NSString*)description :(NSString*)timeout :(NSString*)title :(NSString*)uid {
|
||||
NSMutableDictionary *data = [[NSMutableDictionary alloc] init];
|
||||
data[@"appearance"] = appearance;
|
||||
data[@"description"] = description;
|
||||
@@ -197,11 +203,35 @@ RCT_EXPORT_METHOD(sendEvent:(NSString *)name
|
||||
[self sendEventWithName:showNotificationAction body:data];
|
||||
}
|
||||
|
||||
- (void)hideNotification:(NSString *)uid {
|
||||
- (void)hideNotification:(NSString*)uid {
|
||||
NSMutableDictionary *data = [[NSMutableDictionary alloc] init];
|
||||
data[@"uid"] = uid;
|
||||
|
||||
[self sendEventWithName:hideNotificationAction body:data];
|
||||
}
|
||||
|
||||
- (void)startRecording:(NSString*)mode :(NSString*)dropboxToken :(BOOL)shouldShare :(NSString*)rtmpStreamKey :(NSString*)rtmpBroadcastID :(NSString*)youtubeStreamKey :(NSString*)youtubeBroadcastID :(NSDictionary*)extraMetadata :(BOOL)transcription {
|
||||
NSDictionary *data = @{
|
||||
@"mode": mode,
|
||||
@"dropboxToken": dropboxToken,
|
||||
@"shouldShare": @(shouldShare),
|
||||
@"rtmpStreamKey": rtmpStreamKey,
|
||||
@"rtmpBroadcastID": rtmpBroadcastID,
|
||||
@"youtubeStreamKey": youtubeStreamKey,
|
||||
@"youtubeBroadcastID": youtubeBroadcastID,
|
||||
@"extraMetadata": extraMetadata,
|
||||
@"transcription": @(transcription)
|
||||
};
|
||||
|
||||
[self sendEventWithName:startRecordingAction body:data];
|
||||
}
|
||||
|
||||
- (void)stopRecording:(NSString*)mode :(BOOL)transcription {
|
||||
NSDictionary *data = @{
|
||||
@"mode": mode,
|
||||
@"transcription": @(transcription)
|
||||
};
|
||||
|
||||
[self sendEventWithName:stopRecordingAction body:data];
|
||||
}
|
||||
@end
|
||||
|
||||
@@ -21,6 +21,11 @@
|
||||
#import "JitsiMeetConferenceOptions.h"
|
||||
#import "JitsiMeetViewDelegate.h"
|
||||
|
||||
typedef NS_ENUM(NSInteger, RecordingMode) {
|
||||
RecordingModeFile,
|
||||
RecordingModeStream
|
||||
};
|
||||
|
||||
@interface JitsiMeetView : UIView
|
||||
|
||||
@property (nonatomic, nullable, weak) id<JitsiMeetViewDelegate> delegate;
|
||||
@@ -49,5 +54,7 @@
|
||||
- (void)toggleCamera;
|
||||
- (void)showNotification:(NSString * _Nonnull)appearance :(NSString * _Nullable)description :(NSString * _Nullable)timeout :(NSString * _Nullable)title :(NSString * _Nullable)uid;
|
||||
- (void)hideNotification:(NSString * _Nullable)uid;
|
||||
- (void)startRecording:(RecordingMode)mode :(NSString * _Nullable)dropboxToken :(BOOL)shouldShare :(NSString * _Nullable)rtmpStreamKey :(NSString * _Nullable)rtmpBroadcastID :(NSString * _Nullable)youtubeStreamKey :(NSString * _Nullable)youtubeBroadcastID :(NSString * _Nullable)extraMetadata :(BOOL)transcription;
|
||||
- (void)stopRecording:(RecordingMode)mode :(BOOL)transcription;
|
||||
|
||||
@end
|
||||
|
||||
@@ -30,6 +30,11 @@
|
||||
*/
|
||||
static NSString *const PiPEnabledFeatureFlag = @"pip.enabled";
|
||||
|
||||
/**
|
||||
* Forward declarations.
|
||||
*/
|
||||
static NSString *recordingModeToString(RecordingMode mode);
|
||||
|
||||
|
||||
@implementation JitsiMeetView {
|
||||
/**
|
||||
@@ -142,15 +147,27 @@ static NSString *const PiPEnabledFeatureFlag = @"pip.enabled";
|
||||
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
|
||||
[externalAPI toggleCamera];
|
||||
}
|
||||
|
||||
- (void)showNotification:(NSString *)appearance :(NSString *)description :(NSString *)timeout :(NSString *)title :(NSString *)uid {
|
||||
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
|
||||
[externalAPI showNotification:appearance :description :timeout :title :uid];
|
||||
}
|
||||
|
||||
-(void)hideNotification:(NSString *)uid {
|
||||
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
|
||||
[externalAPI hideNotification:uid];
|
||||
}
|
||||
|
||||
- (void)startRecording:(RecordingMode)mode :(NSString *)dropboxToken :(BOOL)shouldShare :(NSString *)rtmpStreamKey :(NSString *)rtmpBroadcastID :(NSString *)youtubeStreamKey :(NSString *)youtubeBroadcastID :(NSDictionary *)extraMetadata :(BOOL)transcription {
|
||||
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
|
||||
[externalAPI startRecording:recordingModeToString(mode) :dropboxToken :shouldShare :rtmpStreamKey :rtmpBroadcastID :youtubeStreamKey :youtubeBroadcastID :extraMetadata :transcription];
|
||||
}
|
||||
|
||||
- (void)stopRecording:(RecordingMode)mode :(BOOL)transcription {
|
||||
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
|
||||
[externalAPI stopRecording:recordingModeToString(mode) :transcription];
|
||||
}
|
||||
|
||||
#pragma mark Private methods
|
||||
|
||||
- (void)registerObservers {
|
||||
@@ -245,3 +262,14 @@ static NSString *const PiPEnabledFeatureFlag = @"pip.enabled";
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
static NSString *recordingModeToString(RecordingMode mode) {
|
||||
switch (mode) {
|
||||
case RecordingModeFile:
|
||||
return @"file";
|
||||
case RecordingModeStream:
|
||||
return @"stream";
|
||||
default:
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,9 @@
|
||||
"ml": "മലയാളം",
|
||||
"mn": "Монгол",
|
||||
"mr": "मराठी",
|
||||
"nb": "Norsk bokmål",
|
||||
"nl": "Nederlands",
|
||||
"no": "Norsk",
|
||||
"oc": "Occitan",
|
||||
"pl": "Polski",
|
||||
"pt": "Português",
|
||||
|
||||
@@ -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",
|
||||
@@ -70,14 +70,14 @@
|
||||
"breakoutList": "Breakout-Liste",
|
||||
"buttonLabel": "Breakout-Räume",
|
||||
"defaultName": "Breakout-Raum #{{index}}",
|
||||
"hideParticipantList": "Teilnehmerliste ausblenden",
|
||||
"hideParticipantList": "Personenliste ausblenden",
|
||||
"mainRoom": "Hauptraum",
|
||||
"notifications": {
|
||||
"joined": "Breakout-Raum \"{{name}}\" betreten",
|
||||
"joinedMainRoom": "Hauptraum betreten",
|
||||
"joinedTitle": "Breakout-Räume"
|
||||
},
|
||||
"showParticipantList": "Teilnehmerliste anzeigen",
|
||||
"showParticipantList": "Personenliste anzeigen",
|
||||
"title": "Breakout-Räume"
|
||||
},
|
||||
"calendarSync": {
|
||||
@@ -89,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": "An 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",
|
||||
@@ -362,18 +363,18 @@
|
||||
"muteEveryoneDialogModerationOn": "Die Anwesenden können eine Anfrage zum Sprechen jederzeit senden.",
|
||||
"muteEveryoneElseDialog": "Einmal stummgeschaltet, können Sie deren Stummschaltung nicht mehr beenden, aber sie können ihre Stummschaltung jederzeit selbst beenden.",
|
||||
"muteEveryoneElseTitle": "Alle außer {{whom}} stummschalten?",
|
||||
"muteEveryoneElsesVideoDialog": "Sobald die Kamera deaktiviert ist, können Sie sie nicht wieder aktivieren, die Teilnehmer können dies aber jederzeit wieder ändern.",
|
||||
"muteEveryoneElsesVideoDialog": "Sobald die Kamera für alle anderen Personen deaktiviert ist, können Sie diese nicht wieder für alle einschalten, die anderen Personen können ihre Kamera aber jederzeit wieder einschalten.",
|
||||
"muteEveryoneElsesVideoTitle": "Die Kamera von allen außer {{whom}} ausschalten?",
|
||||
"muteEveryoneSelf": "sich selbst",
|
||||
"muteEveryoneStartMuted": "Alle beginnen von jetzt an stummgeschaltet",
|
||||
"muteEveryoneTitle": "Alle stummschalten?",
|
||||
"muteEveryonesVideoDialog": "Sind Sie sicher, dass Sie die Kamera von allen Teilnehmern deaktivieren möchten? Sie können sie nicht wieder aktivieren, die Teilnehmer können dies aber jederzeit wieder ändern.",
|
||||
"muteEveryonesVideoDialog": "Sind Sie sicher, dass Sie die Kamera von allen Personen deaktivieren möchten? Sie können dies nicht wieder rückgängig machen, jede Personen kann ihre Kamera aber jederzeit wieder einschalten.",
|
||||
"muteEveryonesVideoDialogModerationOn": "Die Anwesenden können jederzeit eine Anfrage senden, um ihre Kamera einzuschalten.",
|
||||
"muteEveryonesVideoDialogOk": "deaktivieren",
|
||||
"muteEveryonesVideoTitle": "Die Kamera von allen anderen ausschalten?",
|
||||
"muteParticipantBody": "Sie können die Stummschaltung anderer Personen nicht aufheben, aber eine Person kann ihre eigene Stummschaltung jederzeit beenden.",
|
||||
"muteParticipantButton": "Stummschalten",
|
||||
"muteParticipantsVideoBody": "Sie können die Kamera nicht wieder aktivieren, die Teilnehmer können dies aber jederzeit wieder ändern.",
|
||||
"muteParticipantsVideoBody": "Sie können die Kamera nicht wieder einschalten, die Person kann ihre Kamera aber jederzeit wieder einschalten.",
|
||||
"muteParticipantsVideoBodyModerationOn": "Sie können die Kamera nicht wieder aktivieren und die Person selbst auch nicht.",
|
||||
"muteParticipantsVideoButton": "Kamera ausschalten",
|
||||
"muteParticipantsVideoDialog": "Wollen Sie die Kamera dieser Person wirklich deaktivieren? Sie können die Kamera nicht wieder aktivieren, die Person kann dies aber jederzeit selbst tun.",
|
||||
@@ -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": "\nAn 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.",
|
||||
@@ -641,6 +642,7 @@
|
||||
"on": "Livestream",
|
||||
"onBy": "{{name}} startete den Livestream",
|
||||
"pending": "Livestream wird gestartet …",
|
||||
"policyError": "Sie haben den Livestream zu schnell gestartet. Bitte versuchen Sie es später noch einmal!",
|
||||
"serviceName": "Livestreaming-Dienst",
|
||||
"sessionAlreadyActive": "Diese Konferenz wird bereits als Livestream übertragen.",
|
||||
"signIn": "Mit Google anmelden",
|
||||
@@ -731,14 +733,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 +758,9 @@
|
||||
"gifsMenu": "GIPHY",
|
||||
"groupTitle": "Benachrichtigungen",
|
||||
"hostAskedUnmute": "Die Moderation bittet Sie, das Mikrofon zu aktivieren",
|
||||
"invalidTenant": "Ungültiger Mandant",
|
||||
"invalidTenantHyphenDescription": "Der gewählte Mandantenname ist ungültig (beginnt oder endet mit '-').",
|
||||
"invalidTenantLengthDescription": "Der gewählte Mandantenname ist zu lang.",
|
||||
"invitedOneMember": "{{name}} wurde eingeladen",
|
||||
"invitedThreePlusMembers": "{{name}} und {{count}} andere wurden eingeladen",
|
||||
"invitedTwoMembers": "{{first}} und {{second}} wurden eingeladen",
|
||||
@@ -783,7 +791,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 +819,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 +827,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 +1031,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 +1039,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 +1059,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 schnell gestartet. Bitte versuchen Sie es später noch einmal.",
|
||||
"recordAudioAndVideo": "Kamera und Ton aufzeichnen",
|
||||
"recordTranscription": "Transkription aufzeichnen",
|
||||
"saveLocalRecording": "Aufzeichnung lokal abspeichern",
|
||||
@@ -1073,10 +1083,10 @@
|
||||
"pullToRefresh": "Ziehen, um zu aktualisieren"
|
||||
},
|
||||
"security": {
|
||||
"about": "Sie können Ihre Konferenz mit einem Passwort sichern. Teilnehmer müssen dieses eingeben, bevor sie an der Sitzung teilnehmen dürfen.",
|
||||
"about": "Sie können Ihre Konferenz mit einem Passwort sichern. Personen müssen dieses eingeben, bevor sie an der Sitzung teilnehmen dürfen.",
|
||||
"aboutReadOnly": "Mit Moderationsrechten kann die Konferenz mit einem Passwort gesichert werden. Personen müssen dieses eingeben, bevor sie an der Sitzung teilnehmen dürfen.",
|
||||
"insecureRoomNameWarningNative": "Der Raumname ist unsicher. Unerwünschte Teilnehmer könnten Ihrer Konferenz beitreten. {{recommendAction}} Lernen Sie mehr über die Absicherung Ihrer Konferenz ",
|
||||
"insecureRoomNameWarningWeb": "Der Raumname ist unsicher. Unerwünschte Teilnehmer könnten Ihrer Konferenz beitreten {{recommendAction}} Lernen Sie <a href=\"{{securityUrl}}\" rel=\"security\" target=\"_blank\">hier</a> mehr über die Absicherung Ihrer Konferenz.",
|
||||
"insecureRoomNameWarningNative": "Der Raumname ist unsicher. Unerwünschte Personen könnten Ihrer Konferenz beitreten. {{recommendAction}} Lernen Sie mehr über die Absicherung Ihrer Konferenz ",
|
||||
"insecureRoomNameWarningWeb": "Der Raumname ist unsicher. Unerwünschte Personen könnten Ihrer Konferenz beitreten {{recommendAction}} Lernen Sie <a href=\"{{securityUrl}}\" rel=\"security\" target=\"_blank\">hier</a> mehr über die Absicherung Ihrer Konferenz.",
|
||||
"title": "Sicherheitsoptionen",
|
||||
"unsafeRoomActions": {
|
||||
"meeting": "Erwägen Sie die Absicherung Ihrer Konferenz über den Sicherheits-Button.",
|
||||
@@ -1165,8 +1175,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": {
|
||||
@@ -1176,6 +1186,7 @@
|
||||
"fearful": "Ängstlich",
|
||||
"happy": "Fröhlich",
|
||||
"hours": "{{count}} Std. ",
|
||||
"labelTooltip": "Anzahl der Personen: {{count}}",
|
||||
"minutes": "{{count}} Min. ",
|
||||
"name": "Name",
|
||||
"neutral": "Neutral",
|
||||
@@ -1397,7 +1408,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 +1539,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 +1573,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"
|
||||
|
||||
@@ -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 weren’t 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": "You’re 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": "कनेक्शन जानकारी",
|
||||
|
||||
@@ -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
1584
lang/main-nb.json
Normal file
File diff suppressed because it is too large
Load Diff
1584
lang/main-no.json
Normal file
1584
lang/main-no.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -263,6 +263,7 @@
|
||||
"Remove": "Remove",
|
||||
"Share": "Share",
|
||||
"Submit": "Submit",
|
||||
"Understand": "I understand",
|
||||
"WaitForHostMsg": "The conference has not yet started because no moderators have yet arrived. If you'd like to become a moderator please log-in. Otherwise, please wait.",
|
||||
"WaitForHostNoAuthMsg": "The conference has not yet started because no moderators have yet arrived. Please wait.",
|
||||
"WaitingForHostButton": "Wait for moderator",
|
||||
@@ -393,6 +394,8 @@
|
||||
"recentlyUsedObjects": "Your recently used objects",
|
||||
"recording": "Recording",
|
||||
"recordingDisabledBecauseOfActiveLiveStreamingTooltip": "Not possible while a live stream is active",
|
||||
"recordingInProgressDescription": "This meeting is being recorded. Your audio and video have been muted. If you choose to unmute, you consent to being recorded.",
|
||||
"recordingInProgressTitle": "Recording in progress",
|
||||
"rejoinNow": "Rejoin now",
|
||||
"remoteControlAllowedMessage": "{{user}} accepted your remote control request!",
|
||||
"remoteControlDeniedMessage": "{{user}} rejected your remote control request!",
|
||||
@@ -733,7 +736,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 +760,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 +812,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 +829,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.",
|
||||
|
||||
@@ -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'));
|
||||
@@ -595,9 +596,13 @@ function initCommands() {
|
||||
* Defaults to "normal" if not provided.
|
||||
* @param { string } arg.timeout - Timeout type, either `short`, `medium`, `long` or `sticky`.
|
||||
* Defaults to "short" if not provided.
|
||||
* @param { Array<Object> } arg.customActions - An array of custom actions to be displayed in the notification.
|
||||
* Each object should have a `label` and a `uuid` property. It should be used along a listener
|
||||
* for the `customNotificationActionTriggered` event to handle the custom action.
|
||||
* @returns {void}
|
||||
*/
|
||||
'show-notification': ({
|
||||
customActions = [],
|
||||
title,
|
||||
description,
|
||||
uid,
|
||||
@@ -619,7 +624,15 @@ function initCommands() {
|
||||
return;
|
||||
}
|
||||
|
||||
const handlers = customActions.map(({ uuid }) => () => {
|
||||
APP.API.notifyCustomNotificationActionTriggered(uuid);
|
||||
});
|
||||
|
||||
const keys = customActions.map(({ label }) => label);
|
||||
|
||||
APP.store.dispatch(showNotification({
|
||||
customActionHandler: handlers,
|
||||
customActionNameKey: keys,
|
||||
uid,
|
||||
title,
|
||||
description,
|
||||
@@ -1500,6 +1513,21 @@ class API {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify external application (if API is enabled) that a custom notification action has been triggered.
|
||||
*
|
||||
* @param {string} actionUuid - The UUID of the action that has been triggered.
|
||||
* @returns {void}
|
||||
*/
|
||||
notifyCustomNotificationActionTriggered(actionUuid) {
|
||||
this._sendEvent({
|
||||
name: 'custom-notification-action-triggered',
|
||||
data: {
|
||||
id: actionUuid
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify external application (if API is enabled) that the list of sharing participants changed.
|
||||
*
|
||||
|
||||
1
modules/API/external/external_api.js
vendored
1
modules/API/external/external_api.js
vendored
@@ -114,6 +114,7 @@ const events = {
|
||||
'compute-pressure-changed': 'computePressureChanged',
|
||||
'conference-created-timestamp': 'conferenceCreatedTimestamp',
|
||||
'content-sharing-participants-changed': 'contentSharingParticipantsChanged',
|
||||
'custom-notification-action-triggered': 'customNotificationActionTriggered',
|
||||
'data-channel-closed': 'dataChannelClosed',
|
||||
'data-channel-opened': 'dataChannelOpened',
|
||||
'device-list-changed': 'deviceListChanged',
|
||||
|
||||
@@ -120,7 +120,9 @@ UI.unbindEvents = () => {
|
||||
* @param {string} name etherpad id
|
||||
*/
|
||||
UI.initEtherpad = name => {
|
||||
const etherpadBaseUrl = sanitizeUrl(config.etherpad_base);
|
||||
const { getState, dispatch } = APP.store;
|
||||
const configState = getState()['features/base/config'];
|
||||
const etherpadBaseUrl = sanitizeUrl(configState.etherpad_base);
|
||||
|
||||
if (etherpadManager || !etherpadBaseUrl || !name) {
|
||||
return;
|
||||
@@ -131,9 +133,9 @@ UI.initEtherpad = name => {
|
||||
|
||||
const url = new URL(name, etherpadBaseUrl);
|
||||
|
||||
APP.store.dispatch(setDocumentUrl(url.toString()));
|
||||
dispatch(setDocumentUrl(url.toString()));
|
||||
|
||||
if (config.openSharedDocumentOnJoin) {
|
||||
if (configState.openSharedDocumentOnJoin) {
|
||||
etherpadManager.toggleEtherpad();
|
||||
}
|
||||
};
|
||||
|
||||
10
package-lock.json
generated
10
package-lock.json
generated
@@ -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/v1919.0.0+d4a47d0e/lib-jitsi-meet.tgz",
|
||||
"lodash-es": "4.17.21",
|
||||
"moment": "2.29.4",
|
||||
"moment-duration-format": "2.2.2",
|
||||
@@ -16909,8 +16909,8 @@
|
||||
},
|
||||
"node_modules/lib-jitsi-meet": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/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/v1919.0.0+d4a47d0e/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-0/rTgoaaXwKs4J2+MY4HYh/VbZg3gjNHInhAz+smZGlWsJB8H2qkSNVU0HcTI7WG5LzrzkX4c/eTVpkq8ljLJw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@@ -37637,8 +37637,8 @@
|
||||
}
|
||||
},
|
||||
"lib-jitsi-meet": {
|
||||
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/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/v1919.0.0+d4a47d0e/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-0/rTgoaaXwKs4J2+MY4HYh/VbZg3gjNHInhAz+smZGlWsJB8H2qkSNVU0HcTI7WG5LzrzkX4c/eTVpkq8ljLJw==",
|
||||
"requires": {
|
||||
"@jitsi/js-utils": "2.2.1",
|
||||
"@jitsi/logger": "2.0.2",
|
||||
|
||||
@@ -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/v1919.0.0+d4a47d0e/lib-jitsi-meet.tgz",
|
||||
"lodash-es": "4.17.21",
|
||||
"moment": "2.29.4",
|
||||
"moment-duration-format": "2.2.2",
|
||||
@@ -226,8 +226,11 @@
|
||||
"test-ff-single": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.firefox.conf.ts --spec",
|
||||
"test-ff": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.firefox.conf.ts",
|
||||
"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",
|
||||
|
||||
@@ -152,30 +152,6 @@ export async function createHandlers({ getState }: IStore) {
|
||||
return handlers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a url is a data URL or not.
|
||||
*
|
||||
* @param {string} url - The URL to be checked.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isDataURL(url?: string): boolean {
|
||||
if (typeof url !== 'string') { // The icon will be ignored
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const urlObject = new URL(url);
|
||||
|
||||
if (urlObject.protocol === 'data:') {
|
||||
return false;
|
||||
}
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inits JitsiMeetJS.analytics by setting permanent properties and setting the handlers from the loaded scripts.
|
||||
* NOTE: Has to be used after JitsiMeetJS.init. Otherwise analytics will be null.
|
||||
@@ -208,15 +184,9 @@ export function initAnalytics(store: IStore, handlers: Array<Object>): boolean {
|
||||
isPromotedFromVisitor?: boolean;
|
||||
isVisitor?: boolean;
|
||||
overwritesCustomButtonsWithURL?: boolean;
|
||||
overwritesCustomParticipantButtonsWithURL?: boolean;
|
||||
overwritesDefaultLogoUrl?: boolean;
|
||||
overwritesDeploymentUrls?: boolean;
|
||||
overwritesEtherpadBase?: boolean;
|
||||
overwritesHosts?: boolean;
|
||||
overwritesIceServers?: boolean;
|
||||
overwritesLiveStreamingUrls?: boolean;
|
||||
overwritesPeopleSearchUrl?: boolean;
|
||||
overwritesPrejoinConfigICEUrl?: boolean;
|
||||
overwritesSalesforceUrl?: boolean;
|
||||
overwritesSupportUrl?: boolean;
|
||||
server?: string;
|
||||
@@ -260,19 +230,8 @@ export function initAnalytics(store: IStore, handlers: Array<Object>): boolean {
|
||||
// TODO: Temporary metric. To be removed once we don't need it.
|
||||
permanentProperties.overwritesSupportUrl = 'interfaceConfig.SUPPORT_URL' in params;
|
||||
permanentProperties.overwritesSalesforceUrl = 'config.salesforceUrl' in params;
|
||||
permanentProperties.overwritesPeopleSearchUrl = 'config.peopleSearchUrl' in params;
|
||||
permanentProperties.overwritesDefaultLogoUrl = 'config.defaultLogoUrl' in params;
|
||||
permanentProperties.overwritesEtherpadBase = 'config.etherpad_base' in params;
|
||||
const hosts = params['config.hosts'] ?? {};
|
||||
const hostsProps = [ 'anonymousdomain', 'authdomain', 'domain', 'focus', 'muc', 'visitorFocus' ];
|
||||
|
||||
permanentProperties.overwritesHosts = 'config.hosts' in params
|
||||
|| Boolean(hostsProps.find(p => `config.hosts.${p}` in params || (typeof hosts === 'object' && p in hosts)));
|
||||
|
||||
const prejoinConfig = params['config.prejoinConfig'] ?? {};
|
||||
|
||||
permanentProperties.overwritesPrejoinConfigICEUrl = ('config.prejoinConfig.preCallTestICEUrl' in params)
|
||||
|| (typeof prejoinConfig === 'object' && 'preCallTestICEUrl' in prejoinConfig);
|
||||
const deploymentUrlsConfig = params['config.deploymentUrls'] ?? {};
|
||||
|
||||
permanentProperties.overwritesDeploymentUrls
|
||||
@@ -294,17 +253,7 @@ export function initAnalytics(store: IStore, handlers: Array<Object>): boolean {
|
||||
)
|
||||
);
|
||||
|
||||
permanentProperties.overwritesIceServers = Boolean(Object.keys(params).find(k => k.startsWith('iceServers')));
|
||||
|
||||
const customToolbarButtons = params['config.customToolbarButtons'] ?? [];
|
||||
|
||||
permanentProperties.overwritesCustomButtonsWithURL = Boolean(
|
||||
customToolbarButtons.find(({ icon }: { icon: string; }) => isDataURL(icon)));
|
||||
|
||||
const customParticipantMenuButtons = params['config.customParticipantMenuButtons'] ?? [];
|
||||
|
||||
permanentProperties.overwritesCustomParticipantButtonsWithURL = Boolean(
|
||||
customParticipantMenuButtons.find(({ icon }: { icon: string; }) => isDataURL(icon)));
|
||||
permanentProperties.overwritesCustomButtonsWithURL = 'config.customToolbarButtons' in params;
|
||||
|
||||
// Optionally, include local deployment information based on the
|
||||
// contents of window.config.deploymentInfo.
|
||||
|
||||
@@ -75,7 +75,7 @@ export const _getTokenAuthState = (
|
||||
const params = parseURLParams(locationURL);
|
||||
|
||||
for (const key of Object.keys(params)) {
|
||||
// we allow only config and interfaceConfig overrides in the state
|
||||
// we allow only config, interfaceConfig and iceServers overrides in the state
|
||||
if (key.startsWith('config.') || key.startsWith('interfaceConfig.') || key.startsWith('iceServers.')) {
|
||||
// @ts-ignore
|
||||
state[key] = params[key];
|
||||
|
||||
@@ -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; }) => {
|
||||
|
||||
@@ -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;
|
||||
@@ -540,6 +542,7 @@ export interface IConfig {
|
||||
recordingSharingUrl?: string;
|
||||
recordings?: {
|
||||
recordAudioAndVideo?: boolean;
|
||||
requireConsent?: boolean;
|
||||
showPrejoinWarning?: boolean;
|
||||
showRecordingLink?: boolean;
|
||||
suggestRecording?: boolean;
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { inIframe } from '../util/iframeUtils';
|
||||
|
||||
import extraConfigWhitelist from './extraConfigWhitelist';
|
||||
import inIframeConfigWhitelist from './inIframeConfigWhitelist';
|
||||
|
||||
/**
|
||||
* The config keys to whitelist, the keys that can be overridden.
|
||||
@@ -77,7 +80,6 @@ export default [
|
||||
'channelLastN',
|
||||
'connectionIndicators',
|
||||
'constraints',
|
||||
'customToolbarButtons',
|
||||
'deeplinking.disabled',
|
||||
'deeplinking.desktop.enabled',
|
||||
'defaultLocalDisplayName',
|
||||
@@ -91,6 +93,7 @@ export default [
|
||||
'disableAddingBackgroundImages',
|
||||
'disableAudioLevels',
|
||||
'disableBeforeUnloadHandlers',
|
||||
'disableCameraTintForeground',
|
||||
'disableChatSmileys',
|
||||
'disableDeepLinking',
|
||||
'disabledNotifications',
|
||||
@@ -202,7 +205,10 @@ export default [
|
||||
'remoteVideoMenu',
|
||||
'roomPasswordNumberOfDigits',
|
||||
'readOnlyName',
|
||||
'recordings',
|
||||
'recordings.recordAudioAndVideo',
|
||||
'recordings.showPrejoinWarning',
|
||||
'recordings.showRecordingLink',
|
||||
'recordings.suggestRecording',
|
||||
'replaceParticipant',
|
||||
'resolution',
|
||||
'screenshotCapture',
|
||||
@@ -242,4 +248,4 @@ export default [
|
||||
'webrtcIceTcpDisable',
|
||||
'webrtcIceUdpDisable',
|
||||
'whiteboard.enabled'
|
||||
].concat(extraConfigWhitelist);
|
||||
].concat(extraConfigWhitelist).concat(inIframe() ? inIframeConfigWhitelist : []);
|
||||
|
||||
@@ -327,6 +327,49 @@ export function setConfigFromURLParams(
|
||||
}
|
||||
|
||||
overrideConfigJSON(config, interfaceConfig, json);
|
||||
|
||||
// Print warning about depricated URL params
|
||||
if ('interfaceConfig.SUPPORT_URL' in params) {
|
||||
logger.warn('Using SUPPORT_URL interfaceConfig URL overwrite is deprecated.'
|
||||
+ ' Please use supportUrl from advanced branding!');
|
||||
}
|
||||
|
||||
if ('config.defaultLogoUrl' in params) {
|
||||
logger.warn('Using defaultLogoUrl config URL overwrite is deprecated.'
|
||||
+ ' Please use logoImageUrl from advanced branding!');
|
||||
}
|
||||
|
||||
const deploymentUrlsConfig = params['config.deploymentUrls'] ?? {};
|
||||
|
||||
if ('config.deploymentUrls.downloadAppsUrl' in params || 'config.deploymentUrls.userDocumentationURL' in params
|
||||
|| (typeof deploymentUrlsConfig === 'object'
|
||||
&& ('downloadAppsUrl' in deploymentUrlsConfig || 'userDocumentationURL' in deploymentUrlsConfig))) {
|
||||
logger.warn('Using deploymentUrls config URL overwrite is deprecated.'
|
||||
+ ' Please use downloadAppsUrl and/or userDocumentationURL from advanced branding!');
|
||||
}
|
||||
|
||||
const liveStreamingConfig = params['config.liveStreaming'] ?? {};
|
||||
|
||||
if (('interfaceConfig.LIVE_STREAMING_HELP_LINK' in params)
|
||||
|| ('config.liveStreaming.termsLink' in params)
|
||||
|| ('config.liveStreaming.dataPrivacyLink' in params)
|
||||
|| ('config.liveStreaming.helpLink' in params)
|
||||
|| (typeof params['config.liveStreaming'] === 'object' && 'config.liveStreaming' in params
|
||||
&& (
|
||||
'termsLink' in liveStreamingConfig
|
||||
|| 'dataPrivacyLink' in liveStreamingConfig
|
||||
|| 'helpLink' in liveStreamingConfig
|
||||
)
|
||||
)) {
|
||||
logger.warn('Using liveStreaming config URL overwrite and/or LIVE_STREAMING_HELP_LINK interfaceConfig URL'
|
||||
+ ' overwrite is deprecated. Please use liveStreaming from advanced branding!');
|
||||
}
|
||||
|
||||
if ('config.customToolbarButtons' in params) {
|
||||
logger.warn('Using customToolbarButtons config URL overwrite is deprecated.'
|
||||
+ ' Please use liveStreaming from advanced branding!');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* eslint-enable max-params */
|
||||
|
||||
4
react/features/base/config/inIframeConfigWhitelist.ts
Normal file
4
react/features/base/config/inIframeConfigWhitelist.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Additional config whitelist extending the original whitelist in the case where jitsi-meet is loaded in an iframe.
|
||||
*/
|
||||
export default [];
|
||||
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Additional interface config whitelist extending the original whitelist in the case where jitsi-meet is loaded in an
|
||||
* iframe.
|
||||
*/
|
||||
export default [];
|
||||
@@ -1,4 +1,7 @@
|
||||
import { inIframe } from '../util/iframeUtils';
|
||||
|
||||
import extraInterfaceConfigWhitelistCopy from './extraInterfaceConfigWhitelist';
|
||||
import inIframeInterfaceConfigWhitelist from './inIframeInterfaceConfigWhitelist';
|
||||
|
||||
/**
|
||||
* The interface config keys to whitelist, the keys that can be overridden.
|
||||
@@ -51,4 +54,4 @@ export default [
|
||||
'VERTICAL_FILMSTRIP',
|
||||
'VIDEO_LAYOUT_FIT',
|
||||
'VIDEO_QUALITY_LABEL_DISABLED'
|
||||
].concat(extraInterfaceConfigWhitelistCopy);
|
||||
].concat(extraInterfaceConfigWhitelistCopy).concat(inIframe() ? inIframeInterfaceConfigWhitelist : []);
|
||||
|
||||
@@ -114,11 +114,15 @@ function _setConfig({ dispatch, getState }: IStore, next: Function, action: AnyA
|
||||
function _setDynamicBrandingData({ dispatch }: IStore, next: Function, action: AnyAction) {
|
||||
const config: IConfig = {};
|
||||
const {
|
||||
customParticipantMenuButtons,
|
||||
customToolbarButtons,
|
||||
downloadAppsUrl,
|
||||
etherpadBase,
|
||||
liveStreamingDialogUrls = {},
|
||||
preCallTest = {},
|
||||
salesforceUrl,
|
||||
userDocumentationUrl
|
||||
userDocumentationUrl,
|
||||
peopleSearchUrl,
|
||||
} = action.value;
|
||||
|
||||
const { helpUrl, termsUrl, dataPrivacyUrl } = liveStreamingDialogUrls;
|
||||
@@ -154,6 +158,10 @@ function _setDynamicBrandingData({ dispatch }: IStore, next: Function, action: A
|
||||
config.salesforceUrl = salesforceUrl;
|
||||
}
|
||||
|
||||
if (peopleSearchUrl) {
|
||||
config.peopleSearchUrl = peopleSearchUrl;
|
||||
}
|
||||
|
||||
const { enabled, iceUrl } = preCallTest;
|
||||
|
||||
if (typeof enabled === 'boolean') {
|
||||
@@ -162,11 +170,24 @@ function _setDynamicBrandingData({ dispatch }: IStore, next: Function, action: A
|
||||
};
|
||||
}
|
||||
|
||||
if (etherpadBase) {
|
||||
// eslint-disable-next-line camelcase
|
||||
config.etherpad_base = etherpadBase;
|
||||
}
|
||||
|
||||
if (iceUrl) {
|
||||
config.prejoinConfig = config.prejoinConfig || {};
|
||||
config.prejoinConfig.preCallTestICEUrl = iceUrl;
|
||||
}
|
||||
|
||||
if (customToolbarButtons) {
|
||||
config.customToolbarButtons = customToolbarButtons;
|
||||
}
|
||||
|
||||
if (customParticipantMenuButtons) {
|
||||
config.customParticipantMenuButtons = customParticipantMenuButtons;
|
||||
}
|
||||
|
||||
dispatch(updateConfig(config));
|
||||
|
||||
return next(action);
|
||||
|
||||
@@ -5,6 +5,7 @@ import { conferenceLeft, conferenceWillLeave, redirect } from '../conference/act
|
||||
import { getCurrentConference } from '../conference/functions';
|
||||
import { IConfigState } from '../config/reducer';
|
||||
import JitsiMeetJS, { JitsiConnectionEvents } from '../lib-jitsi-meet';
|
||||
import { inIframe } from '../util/iframeUtils';
|
||||
import { parseURLParams } from '../util/parseURLParams';
|
||||
import {
|
||||
appendURLParam,
|
||||
@@ -119,7 +120,8 @@ export function constructOptions(state: IReduxState) {
|
||||
const params = parseURLParams(locationURL || '');
|
||||
const iceServersOverride = params['iceServers.replace'];
|
||||
|
||||
if (iceServersOverride) {
|
||||
// Allow iceServersOverride only when jitsi-meet is in an iframe.
|
||||
if (inIframe() && iceServersOverride) {
|
||||
options.iceServersOverride = iceServersOverride;
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,11 @@ interface IProps extends AbstractProps, WithTranslation {
|
||||
*/
|
||||
descriptionKey?: string | { key: string; params: string; };
|
||||
|
||||
/**
|
||||
* Whether the cancel button is hidden.
|
||||
*/
|
||||
isCancelHidden?: Boolean;
|
||||
|
||||
/**
|
||||
* Whether or not the nature of the confirm button is destructive.
|
||||
*/
|
||||
@@ -100,6 +105,7 @@ class ConfirmDialog extends AbstractDialog<IProps> {
|
||||
cancelLabel,
|
||||
children,
|
||||
confirmLabel,
|
||||
isCancelHidden,
|
||||
isConfirmDestructive,
|
||||
isConfirmHidden,
|
||||
t,
|
||||
@@ -121,10 +127,12 @@ class ConfirmDialog extends AbstractDialog<IProps> {
|
||||
}
|
||||
{ this._renderDescription() }
|
||||
{ children }
|
||||
<Dialog.Button
|
||||
label = { t(cancelLabel || 'dialog.confirmNo') }
|
||||
onPress = { this._onCancel }
|
||||
style = { styles.dialogButton } />
|
||||
{
|
||||
!isCancelHidden && <Dialog.Button
|
||||
label = { t(cancelLabel || 'dialog.confirmNo') }
|
||||
onPress = { this._onCancel }
|
||||
style = { styles.dialogButton } />
|
||||
}
|
||||
{
|
||||
!isConfirmHidden && <Dialog.Button
|
||||
label = { t(confirmLabel || 'dialog.confirmYes') }
|
||||
|
||||
@@ -4,6 +4,12 @@
|
||||
*/
|
||||
export const ADD_PEOPLE_ENABLED = 'add-people.enabled';
|
||||
|
||||
/**
|
||||
* Flag indicating if the audio device button should be displayed.
|
||||
* Default: enabled (true).
|
||||
*/
|
||||
export const AUDIO_DEVICE_BUTTON_ENABLED = 'audio-device-button.enabled';
|
||||
|
||||
/**
|
||||
* Flag indicating if the SDK should not require the audio focus.
|
||||
* Used by apps that do not use Jitsi audio.
|
||||
@@ -239,10 +245,16 @@ export const SETTINGS_ENABLED = 'settings.enabled';
|
||||
|
||||
/**
|
||||
* Flag indicating if tile view feature should be enabled.
|
||||
* Default: enabled.
|
||||
* Default: enabled(true).
|
||||
*/
|
||||
export const TILE_VIEW_ENABLED = 'tile-view.enabled';
|
||||
|
||||
/**
|
||||
* Flag indicating if the toggle camera button should be enabled
|
||||
* Default: enabled(true).
|
||||
*/
|
||||
export const TOGGLE_CAMERA_BUTTON_ENABLED = 'toggle-camera-button.enabled';
|
||||
|
||||
/**
|
||||
* Flag indicating if the toolbox should be always be visible
|
||||
* Default: disabled (false).
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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' ],
|
||||
|
||||
@@ -838,16 +838,6 @@ export function toggleCamera() {
|
||||
const localVideoTrack = getLocalVideoTrack(tracks)?.jitsiTrack;
|
||||
const currentFacingMode = localVideoTrack.getCameraFacingMode();
|
||||
const { localFlipX } = state['features/base/settings'];
|
||||
|
||||
/**
|
||||
* FIXME: Ideally, we should be dispatching {@code replaceLocalTrack} here,
|
||||
* but it seems to not trigger the re-rendering of the local video on Chrome;
|
||||
* could be due to a plan B vs unified plan issue. Therefore, we use the legacy
|
||||
* method defined in conference.js that manually takes care of updating the local
|
||||
* video as well.
|
||||
*/
|
||||
await APP.conference.useVideoStream(null);
|
||||
|
||||
const targetFacingMode = currentFacingMode === CAMERA_FACING_MODE.USER
|
||||
? CAMERA_FACING_MODE.ENVIRONMENT
|
||||
: CAMERA_FACING_MODE.USER;
|
||||
@@ -857,7 +847,6 @@ export function toggleCamera() {
|
||||
|
||||
const newVideoTrack = await createLocalTrack('video', null, null, { facingMode: targetFacingMode });
|
||||
|
||||
// FIXME: See above.
|
||||
await APP.conference.useVideoStream(newVideoTrack);
|
||||
dispatch(replaceLocalTrack(localVideoTrack, newVideoTrack));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import { connect, useDispatch } from 'react-redux';
|
||||
import { appNavigate } from '../../../app/actions.native';
|
||||
import { IReduxState, IStore } from '../../../app/types';
|
||||
import { CONFERENCE_BLURRED, CONFERENCE_FOCUSED } from '../../../base/conference/actionTypes';
|
||||
import { isDisplayNameVisible } from '../../../base/config/functions.native';
|
||||
import { FULLSCREEN_ENABLED } from '../../../base/flags/constants';
|
||||
import { getFeatureFlag } from '../../../base/flags/functions';
|
||||
import Container from '../../../base/react/components/native/Container';
|
||||
@@ -100,6 +101,11 @@ interface IProps extends AbstractProps {
|
||||
*/
|
||||
_fullscreenEnabled: boolean;
|
||||
|
||||
/**
|
||||
* The indicator which determines if the display name is visible.
|
||||
*/
|
||||
_isDisplayNameVisible: boolean;
|
||||
|
||||
/**
|
||||
* The indicator which determines if the participants pane is open.
|
||||
*/
|
||||
@@ -364,6 +370,7 @@ class Conference extends AbstractConference<IProps, State> {
|
||||
_aspectRatio,
|
||||
_connecting,
|
||||
_filmstripVisible,
|
||||
_isDisplayNameVisible,
|
||||
_largeVideoParticipantId,
|
||||
_reducedUI,
|
||||
_shouldDisplayTileView,
|
||||
@@ -420,10 +427,12 @@ class Conference extends AbstractConference<IProps, State> {
|
||||
|
||||
{
|
||||
_shouldDisplayTileView
|
||||
|| <Container style = { styles.displayNameContainer }>
|
||||
<DisplayNameLabel
|
||||
participantId = { _largeVideoParticipantId } />
|
||||
</Container>
|
||||
|| (_isDisplayNameVisible && (
|
||||
<Container style = { styles.displayNameContainer }>
|
||||
<DisplayNameLabel
|
||||
participantId = { _largeVideoParticipantId } />
|
||||
</Container>
|
||||
))
|
||||
}
|
||||
|
||||
{ !_shouldDisplayTileView && <LonelyMeetingExperience /> }
|
||||
@@ -577,6 +586,7 @@ function _mapStateToProps(state: IReduxState, _ownProps: any) {
|
||||
_connecting: isConnecting(state),
|
||||
_filmstripVisible: isFilmstripVisible(state),
|
||||
_fullscreenEnabled: getFeatureFlag(state, FULLSCREEN_ENABLED, true),
|
||||
_isDisplayNameVisible: isDisplayNameVisible(state),
|
||||
_isParticipantsPaneOpen: isOpen,
|
||||
_largeVideoParticipantId: state['features/large-video'].participantId,
|
||||
_pictureInPictureEnabled: isPipEnabled(state),
|
||||
|
||||
@@ -82,6 +82,7 @@ class LonelyMeetingExperience extends PureComponent<IProps> {
|
||||
render() {
|
||||
const {
|
||||
_inviteOthersControl,
|
||||
_isAddPeopleFeatureEnabled,
|
||||
_isInBreakoutRoom,
|
||||
_isInviteFunctionsDisabled,
|
||||
_isLonelyMeeting,
|
||||
@@ -89,7 +90,7 @@ class LonelyMeetingExperience extends PureComponent<IProps> {
|
||||
} = this.props;
|
||||
const { color, shareDialogVisible } = _inviteOthersControl;
|
||||
|
||||
if (!_isLonelyMeeting) {
|
||||
if (!_isLonelyMeeting || !_isAddPeopleFeatureEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,13 +4,17 @@ import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { getConferenceName, getConferenceTimestamp } from '../../../base/conference/functions';
|
||||
import { CONFERENCE_TIMER_ENABLED } from '../../../base/flags/constants';
|
||||
import {
|
||||
AUDIO_DEVICE_BUTTON_ENABLED,
|
||||
CONFERENCE_TIMER_ENABLED,
|
||||
TOGGLE_CAMERA_BUTTON_ENABLED
|
||||
} from '../../../base/flags/constants';
|
||||
import { getFeatureFlag } from '../../../base/flags/functions';
|
||||
import AudioDeviceToggleButton from '../../../mobile/audio-mode/components/AudioDeviceToggleButton';
|
||||
import PictureInPictureButton from '../../../mobile/picture-in-picture/components/PictureInPictureButton';
|
||||
import ParticipantsPaneButton from '../../../participants-pane/components/native/ParticipantsPaneButton';
|
||||
import { isParticipantsPaneEnabled } from '../../../participants-pane/functions';
|
||||
import { isRoomNameEnabled } from '../../../prejoin/functions';
|
||||
import { isRoomNameEnabled } from '../../../prejoin/functions.native';
|
||||
import ToggleCameraButton from '../../../toolbox/components/native/ToggleCameraButton';
|
||||
import { isToolboxVisible } from '../../../toolbox/functions.native';
|
||||
import ConferenceTimer from '../ConferenceTimer';
|
||||
@@ -21,6 +25,11 @@ import styles from './styles';
|
||||
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* Whether the audio device button should be displayed.
|
||||
*/
|
||||
_audioDeviceButtonEnabled: boolean;
|
||||
|
||||
/**
|
||||
* Whether displaying the current conference timer is enabled or not.
|
||||
*/
|
||||
@@ -47,6 +56,11 @@ interface IProps {
|
||||
*/
|
||||
_roomNameEnabled: boolean;
|
||||
|
||||
/**
|
||||
* Whether the toggle camera button should be displayed.
|
||||
*/
|
||||
_toggleCameraButtonEnabled: boolean;
|
||||
|
||||
/**
|
||||
* True if the navigation bar should be visible.
|
||||
*/
|
||||
@@ -95,12 +109,18 @@ const TitleBar = (props: IProps) => {
|
||||
{/* eslint-disable-next-line react/jsx-no-bind */}
|
||||
<Labels createOnPress = { props._createOnPress } />
|
||||
</View>
|
||||
<View style = { styles.titleBarButtonContainer }>
|
||||
<ToggleCameraButton styles = { styles.titleBarButton } />
|
||||
</View>
|
||||
<View style = { styles.titleBarButtonContainer }>
|
||||
<AudioDeviceToggleButton styles = { styles.titleBarButton } />
|
||||
</View>
|
||||
{
|
||||
props._toggleCameraButtonEnabled
|
||||
&& <View style = { styles.titleBarButtonContainer }>
|
||||
<ToggleCameraButton styles = { styles.titleBarButton } />
|
||||
</View>
|
||||
}
|
||||
{
|
||||
props._audioDeviceButtonEnabled
|
||||
&& <View style = { styles.titleBarButtonContainer }>
|
||||
<AudioDeviceToggleButton styles = { styles.titleBarButton } />
|
||||
</View>
|
||||
}
|
||||
{
|
||||
_isParticipantsPaneEnabled
|
||||
&& <View style = { styles.titleBarButtonContainer }>
|
||||
@@ -123,11 +143,13 @@ function _mapStateToProps(state: IReduxState) {
|
||||
const startTimestamp = getConferenceTimestamp(state);
|
||||
|
||||
return {
|
||||
_audioDeviceButtonEnabled: getFeatureFlag(state, AUDIO_DEVICE_BUTTON_ENABLED, true),
|
||||
_conferenceTimerEnabled:
|
||||
Boolean(getFeatureFlag(state, CONFERENCE_TIMER_ENABLED, true) && !hideConferenceTimer && startTimestamp),
|
||||
_isParticipantsPaneEnabled: isParticipantsPaneEnabled(state),
|
||||
_meetingName: getConferenceName(state),
|
||||
_roomNameEnabled: isRoomNameEnabled(state),
|
||||
_toggleCameraButtonEnabled: getFeatureFlag(state, TOGGLE_CAMERA_BUTTON_ENABLED, true),
|
||||
_visible: isToolboxVisible(state)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -22,15 +22,19 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
backgroundColor,
|
||||
backgroundImageUrl,
|
||||
brandedIcons,
|
||||
customParticipantMenuButtons,
|
||||
customToolbarButtons,
|
||||
didPageUrl,
|
||||
downloadAppsUrl,
|
||||
etherpadBase,
|
||||
inviteDomain,
|
||||
labels,
|
||||
liveStreamingDialogUrls,
|
||||
peopleSearchUrl,
|
||||
salesforceUrl,
|
||||
sharedVideoAllowedURLDomains,
|
||||
supportUrl,
|
||||
userDocumentationUrl
|
||||
userDocumentationUrl,
|
||||
} = action.value;
|
||||
|
||||
action.value = {
|
||||
@@ -38,11 +42,15 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
backgroundColor,
|
||||
backgroundImageUrl,
|
||||
brandedIcons,
|
||||
customParticipantMenuButtons,
|
||||
customToolbarButtons,
|
||||
didPageUrl,
|
||||
downloadAppsUrl,
|
||||
etherpadBase,
|
||||
inviteDomain,
|
||||
labels,
|
||||
liveStreamingDialogUrls,
|
||||
peopleSearchUrl,
|
||||
salesforceUrl,
|
||||
sharedVideoAllowedURLDomains,
|
||||
supportUrl,
|
||||
|
||||
@@ -158,6 +158,7 @@ export interface IDynamicBrandingState {
|
||||
logoImageUrl: string;
|
||||
muiBrandedTheme?: boolean;
|
||||
premeetingBackground: string;
|
||||
requireRecordingConsent?: boolean;
|
||||
sharedVideoAllowedURLDomains?: Array<string>;
|
||||
showGiphyIntegration?: boolean;
|
||||
supportUrl?: string;
|
||||
@@ -186,6 +187,7 @@ ReducerRegistry.register<IDynamicBrandingState>(STORE_NAME, (state = DEFAULT_STA
|
||||
premeetingBackground,
|
||||
sharedVideoAllowedURLDomains,
|
||||
showGiphyIntegration,
|
||||
requireRecordingConsent,
|
||||
supportUrl,
|
||||
virtualBackgrounds
|
||||
} = action.value;
|
||||
@@ -205,6 +207,7 @@ ReducerRegistry.register<IDynamicBrandingState>(STORE_NAME, (state = DEFAULT_STA
|
||||
premeetingBackground,
|
||||
sharedVideoAllowedURLDomains,
|
||||
showGiphyIntegration,
|
||||
requireRecordingConsent,
|
||||
supportUrl,
|
||||
customizationFailed: false,
|
||||
customizationReady: true,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -4,6 +4,7 @@ import { connect } from 'react-redux';
|
||||
// @ts-expect-error
|
||||
import VideoLayout from '../../../../modules/UI/videolayout/VideoLayout';
|
||||
import { IReduxState, IStore } from '../../app/types';
|
||||
import { isDisplayNameVisible } from '../../base/config/functions.web';
|
||||
import { VIDEO_TYPE } from '../../base/media/constants';
|
||||
import { getLocalParticipant } from '../../base/participants/functions';
|
||||
import Watermarks from '../../base/react/components/web/Watermarks';
|
||||
@@ -58,6 +59,11 @@ interface IProps {
|
||||
*/
|
||||
_isChatOpen: boolean;
|
||||
|
||||
/**
|
||||
* Whether or not the display name is visible.
|
||||
*/
|
||||
_isDisplayNameVisible: boolean;
|
||||
|
||||
/**
|
||||
* Whether or not the local screen share is on large-video.
|
||||
*/
|
||||
@@ -191,6 +197,7 @@ class LargeVideo extends Component<IProps> {
|
||||
const {
|
||||
_displayScreenSharingPlaceholder,
|
||||
_isChatOpen,
|
||||
_isDisplayNameVisible,
|
||||
_noAutoPlayVideo,
|
||||
_showDominantSpeakerBadge,
|
||||
_whiteboardEnabled
|
||||
@@ -243,7 +250,12 @@ class LargeVideo extends Component<IProps> {
|
||||
</div>
|
||||
{ interfaceConfig.DISABLE_TRANSCRIPTION_SUBTITLES
|
||||
|| <Captions /> }
|
||||
{_showDominantSpeakerBadge && <StageParticipantNameLabel />}
|
||||
{
|
||||
_isDisplayNameVisible
|
||||
&& (
|
||||
_showDominantSpeakerBadge && <StageParticipantNameLabel />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -368,6 +380,7 @@ function _mapStateToProps(state: IReduxState) {
|
||||
_displayScreenSharingPlaceholder: Boolean(isLocalScreenshareOnLargeVideo && !seeWhatIsBeingShared && !isOnSpot),
|
||||
_hideSelfView: getHideSelfView(state),
|
||||
_isChatOpen: isChatOpen,
|
||||
_isDisplayNameVisible: isDisplayNameVisible(state),
|
||||
_isScreenSharing: Boolean(isLocalScreenshareOnLargeVideo),
|
||||
_largeVideoParticipantId: largeVideoParticipant?.id ?? '',
|
||||
_localParticipantId: localParticipantId ?? '',
|
||||
|
||||
@@ -4,7 +4,7 @@ import { debounce } from 'lodash-es';
|
||||
import { NativeEventEmitter, NativeModules } from 'react-native';
|
||||
import { AnyAction } from 'redux';
|
||||
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
import { ENDPOINT_TEXT_MESSAGE_NAME } from '../../../../modules/API/constants';
|
||||
import { appNavigate } from '../../app/actions.native';
|
||||
import { IStore } from '../../app/types';
|
||||
@@ -32,8 +32,7 @@ import {
|
||||
JITSI_CONNECTION_URL_KEY
|
||||
} from '../../base/connection/constants';
|
||||
import { getURLWithoutParams } from '../../base/connection/utils';
|
||||
import {
|
||||
JitsiConferenceEvents } from '../../base/lib-jitsi-meet';
|
||||
import { JitsiConferenceEvents, JitsiRecordingConstants } from '../../base/lib-jitsi-meet';
|
||||
import { SET_AUDIO_MUTED, SET_VIDEO_MUTED } from '../../base/media/actionTypes';
|
||||
import { toggleCameraFacingMode } from '../../base/media/actions';
|
||||
import { MEDIA_TYPE, VIDEO_TYPE } from '../../base/media/constants';
|
||||
@@ -51,8 +50,11 @@ import { getLocalTracks, isLocalTrackMuted } from '../../base/tracks/functions.n
|
||||
import { ITrack } from '../../base/tracks/types';
|
||||
import { CLOSE_CHAT, OPEN_CHAT } from '../../chat/actionTypes';
|
||||
import { closeChat, openChat, sendMessage, setPrivateMessageRecipient } from '../../chat/actions.native';
|
||||
import { isEnabled as isDropboxEnabled } from '../../dropbox/functions.native';
|
||||
import { hideNotification, showNotification } from '../../notifications/actions';
|
||||
import { NOTIFICATION_TIMEOUT_TYPE, NOTIFICATION_TYPE } from '../../notifications/constants';
|
||||
import { RECORDING_METADATA_ID, RECORDING_TYPES } from '../../recording/constants';
|
||||
import { getActiveSession } from '../../recording/functions';
|
||||
import { setRequestingSubtitles } from '../../subtitles/actions.any';
|
||||
import { CUSTOM_BUTTON_PRESSED } from '../../toolbox/actionTypes';
|
||||
import { muteLocal } from '../../video-menu/actions.native';
|
||||
@@ -450,6 +452,129 @@ function _registerForNativeEvents(store: IStore) {
|
||||
eventEmitter.addListener(ExternalAPI.HIDE_NOTIFICATION, ({ uid }: any) => {
|
||||
dispatch(hideNotification(uid));
|
||||
});
|
||||
|
||||
eventEmitter.addListener(ExternalAPI.START_RECORDING, (
|
||||
{
|
||||
mode,
|
||||
dropboxToken,
|
||||
shouldShare,
|
||||
rtmpStreamKey,
|
||||
rtmpBroadcastID,
|
||||
youtubeStreamKey,
|
||||
youtubeBroadcastID,
|
||||
extraMetadata = {},
|
||||
transcription
|
||||
}: any) => {
|
||||
const state = store.getState();
|
||||
const conference = getCurrentConference(state);
|
||||
|
||||
if (!conference) {
|
||||
logger.error('Conference is not defined');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (dropboxToken && !isDropboxEnabled(state)) {
|
||||
logger.error('Failed starting recording: dropbox is not enabled on this deployment');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode === JitsiRecordingConstants.mode.STREAM && !(youtubeStreamKey || rtmpStreamKey)) {
|
||||
logger.error('Failed starting recording: missing youtube or RTMP stream key');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let recordingConfig;
|
||||
|
||||
if (mode === JitsiRecordingConstants.mode.FILE) {
|
||||
const { recordingService } = state['features/base/config'];
|
||||
|
||||
if (!recordingService?.enabled && !dropboxToken) {
|
||||
logger.error('Failed starting recording: the recording service is not enabled');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (dropboxToken) {
|
||||
recordingConfig = {
|
||||
mode: JitsiRecordingConstants.mode.FILE,
|
||||
appData: JSON.stringify({
|
||||
'file_recording_metadata': {
|
||||
...extraMetadata,
|
||||
'upload_credentials': {
|
||||
'service_name': RECORDING_TYPES.DROPBOX,
|
||||
'token': dropboxToken
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
} else {
|
||||
recordingConfig = {
|
||||
mode: JitsiRecordingConstants.mode.FILE,
|
||||
appData: JSON.stringify({
|
||||
'file_recording_metadata': {
|
||||
...extraMetadata,
|
||||
'share': shouldShare
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
} else if (mode === JitsiRecordingConstants.mode.STREAM) {
|
||||
recordingConfig = {
|
||||
broadcastId: youtubeBroadcastID || rtmpBroadcastID,
|
||||
mode: JitsiRecordingConstants.mode.STREAM,
|
||||
streamId: youtubeStreamKey || rtmpStreamKey
|
||||
};
|
||||
}
|
||||
|
||||
// Start audio / video recording, if requested.
|
||||
if (typeof recordingConfig !== 'undefined') {
|
||||
conference.startRecording(recordingConfig);
|
||||
}
|
||||
|
||||
if (transcription) {
|
||||
store.dispatch(setRequestingSubtitles(true, false, null));
|
||||
conference.getMetadataHandler().setMetadata(RECORDING_METADATA_ID, {
|
||||
isTranscribingEnabled: true
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
eventEmitter.addListener(ExternalAPI.STOP_RECORDING, ({ mode, transcription }: any) => {
|
||||
const state = store.getState();
|
||||
const conference = getCurrentConference(state);
|
||||
|
||||
if (!conference) {
|
||||
logger.error('Conference is not defined');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (transcription) {
|
||||
store.dispatch(setRequestingSubtitles(false, false, null));
|
||||
conference.getMetadataHandler().setMetadata(RECORDING_METADATA_ID, {
|
||||
isTranscribingEnabled: false
|
||||
});
|
||||
}
|
||||
|
||||
if (![ JitsiRecordingConstants.mode.FILE, JitsiRecordingConstants.mode.STREAM ].includes(mode)) {
|
||||
logger.error('Invalid recording mode provided!');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const activeSession = getActiveSession(state, mode);
|
||||
|
||||
if (!activeSession?.id) {
|
||||
logger.error('No recording or streaming session found');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
conference.stopRecording(activeSession.id);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -472,6 +597,8 @@ function _unregisterForNativeEvents() {
|
||||
eventEmitter.removeAllListeners(ExternalAPI.TOGGLE_CAMERA);
|
||||
eventEmitter.removeAllListeners(ExternalAPI.SHOW_NOTIFICATION);
|
||||
eventEmitter.removeAllListeners(ExternalAPI.HIDE_NOTIFICATION);
|
||||
eventEmitter.removeAllListeners(ExternalAPI.START_RECORDING);
|
||||
eventEmitter.removeAllListeners(ExternalAPI.STOP_RECORDING);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 ]);
|
||||
|
||||
/*
|
||||
|
||||
@@ -68,12 +68,16 @@ function _toDateString(itemDate: number, t: Function) {
|
||||
const dateInMs = date.getTime();
|
||||
const now = new Date();
|
||||
const todayInMs = new Date().setHours(0, 0, 0, 0);
|
||||
const yesterdayInMs = todayInMs - 86400000; // 1 day = 86400000ms
|
||||
const yesterday = new Date();
|
||||
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
yesterday.setHours(0, 0, 0, 0);
|
||||
const yesterdayInMs = yesterday.getTime();
|
||||
|
||||
if (dateInMs >= todayInMs) {
|
||||
return m.fromNow();
|
||||
} else if (dateInMs >= yesterdayInMs) {
|
||||
return t('dateUtils.yesterday');
|
||||
return `${t('dateUtils.yesterday')}, ${m.format('h:mm A')}`;
|
||||
} else if (date.getFullYear() !== now.getFullYear()) {
|
||||
// We only want to include the year in the date if its not the current
|
||||
// year.
|
||||
|
||||
@@ -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));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export { default as StartRecordingDialog } from './native/StartRecordingDialog';
|
||||
export { default as RecordingConsentDialog } from './native/RecordingConsentDialog';
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export { default as StartRecordingDialog } from './web/StartRecordingDialog';
|
||||
export { default as RecordingConsentDialog } from './web/RecordingConsentDialog';
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import ConfirmDialog from '../../../../base/dialog/components/native/ConfirmDialog';
|
||||
import { setAudioUnmutePermissions, setVideoUnmutePermissions } from '../../../../base/media/actions';
|
||||
|
||||
/**
|
||||
* Component that renders the dialog for explicit consent for recordings.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
export default function RecordingConsentDialog() {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const consent = useCallback(() => {
|
||||
dispatch(setAudioUnmutePermissions(false, true));
|
||||
dispatch(setVideoUnmutePermissions(false, true));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ConfirmDialog
|
||||
confirmLabel = { 'dialog.Understand' }
|
||||
descriptionKey = { 'dialog.recordingInProgressDescription' }
|
||||
isCancelHidden = { true }
|
||||
onSubmit = { consent }
|
||||
title = { 'dialog.recordingInProgressTitle' } />
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { setAudioUnmutePermissions, setVideoUnmutePermissions } from '../../../../base/media/actions';
|
||||
import Dialog from '../../../../base/ui/components/web/Dialog';
|
||||
|
||||
/**
|
||||
* Component that renders the dialog for explicit consent for recordings.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
export default function RecordingConsentDialog() {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const consent = useCallback(() => {
|
||||
dispatch(setAudioUnmutePermissions(false, true));
|
||||
dispatch(setVideoUnmutePermissions(false, true));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
cancel = {{ hidden: true }}
|
||||
disableBackdropClose = { true }
|
||||
ok = {{ translationKey: 'dialog.Understand' }}
|
||||
onSubmit = { consent }
|
||||
titleKey = 'dialog.recordingInProgressTitle'>
|
||||
<div>
|
||||
{t('dialog.recordingInProgressDescription')}
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -437,3 +437,26 @@ export function isLiveStreamingButtonVisible({
|
||||
}) {
|
||||
return !isInBreakoutRoom && liveStreamingEnabled && liveStreamingAllowed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the RecordingConsentDialog should be displayed.
|
||||
*
|
||||
* @param {any} recorderSession - The recorder session.
|
||||
* @param {IReduxState} state - The Redux state.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function shouldRequireRecordingConsent(recorderSession: any, state: IReduxState) {
|
||||
const { requireRecordingConsent } = state['features/dynamic-branding'] || {};
|
||||
const { requireConsent } = state['features/base/config'].recordings || {};
|
||||
|
||||
if (!requireConsent && !requireRecordingConsent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!recorderSession._initiator
|
||||
|| recorderSession._statusFromJicofo === JitsiRecordingConstants.status.OFF) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return recorderSession._initiator !== getLocalParticipant(state)?.id;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
import { batch } from 'react-redux';
|
||||
|
||||
import { createRecordingEvent } from '../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../analytics/functions';
|
||||
import { IStore } from '../app/types';
|
||||
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app/actionTypes';
|
||||
import { CONFERENCE_JOIN_IN_PROGRESS } from '../base/conference/actionTypes';
|
||||
import { getCurrentConference } from '../base/conference/functions';
|
||||
import { openDialog } from '../base/dialog/actions';
|
||||
import JitsiMeetJS, {
|
||||
JitsiConferenceEvents,
|
||||
JitsiRecordingConstants
|
||||
} from '../base/lib-jitsi-meet';
|
||||
import {
|
||||
setAudioMuted,
|
||||
setAudioUnmutePermissions,
|
||||
setVideoMuted,
|
||||
setVideoUnmutePermissions
|
||||
} from '../base/media/actions';
|
||||
import { MEDIA_TYPE } from '../base/media/constants';
|
||||
import { PARTICIPANT_UPDATED } from '../base/participants/actionTypes';
|
||||
import { updateLocalRecordingStatus } from '../base/participants/actions';
|
||||
@@ -37,6 +46,7 @@ import {
|
||||
showStoppedRecordingNotification,
|
||||
updateRecordingSessionData
|
||||
} from './actions';
|
||||
import { RecordingConsentDialog } from './components/Recording';
|
||||
import LocalRecordingManager from './components/Recording/LocalRecordingManager';
|
||||
import {
|
||||
LIVE_STREAMING_OFF_SOUND_ID,
|
||||
@@ -49,6 +59,7 @@ import {
|
||||
getResourceId,
|
||||
getSessionById,
|
||||
registerRecordingAudioFiles,
|
||||
shouldRequireRecordingConsent,
|
||||
unregisterRecordingAudioFiles
|
||||
} from './functions';
|
||||
import logger from './logger';
|
||||
@@ -101,7 +112,11 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
(recorderSession: any) => {
|
||||
if (recorderSession) {
|
||||
recorderSession.getID() && dispatch(updateRecordingSessionData(recorderSession));
|
||||
recorderSession.getError() && _showRecordingErrorNotification(recorderSession, dispatch, getState);
|
||||
if (recorderSession.getError()) {
|
||||
_showRecordingErrorNotification(recorderSession, dispatch, getState);
|
||||
} else {
|
||||
_showExplicitConsentDialog(recorderSession, dispatch, getState);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -390,3 +405,25 @@ function _showRecordingErrorNotification(session: any, dispatch: IStore['dispatc
|
||||
APP.API.notifyRecordingStatusChanged(false, mode, error, isRecorderTranscriptionsRunning(getState()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mutes audio and video and displays the RecordingConsentDialog when the conditions are met.
|
||||
*
|
||||
* @param {any} recorderSession - The recording session.
|
||||
* @param {Function} dispatch - The Redux dispatch function.
|
||||
* @param {Function} getState - The Redux getState function.
|
||||
* @returns {void}
|
||||
*/
|
||||
function _showExplicitConsentDialog(recorderSession: any, dispatch: IStore['dispatch'], getState: IStore['getState']) {
|
||||
if (!shouldRequireRecordingConsent(recorderSession, getState())) {
|
||||
return;
|
||||
}
|
||||
|
||||
batch(() => {
|
||||
dispatch(setAudioUnmutePermissions(true, true));
|
||||
dispatch(setVideoUnmutePermissions(true, true));
|
||||
dispatch(setAudioMuted(true));
|
||||
dispatch(setVideoMuted(true));
|
||||
dispatch(openDialog(RecordingConsentDialog));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -114,6 +114,7 @@ const ProfileView = ({ isInWelcomePage }: {
|
||||
return (
|
||||
<JitsiScreen
|
||||
disableForcedKeyboardDismiss = { true }
|
||||
hasBottomTextInput = { true }
|
||||
|
||||
// @ts-ignore
|
||||
safeAreaInsets = { [ !isInWelcomePage && 'bottom', 'left', 'right' ].filter(Boolean) as Edge[] }
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -200,8 +200,14 @@ end
|
||||
|
||||
-- Managing breakout rooms
|
||||
|
||||
function create_breakout_room(room_jid, subject)
|
||||
local main_room, main_room_jid = get_main_room(room_jid);
|
||||
function create_breakout_room(orig_room, subject)
|
||||
local main_room, main_room_jid = get_main_room(orig_room.jid);
|
||||
|
||||
if orig_room ~= main_room then
|
||||
module:log('warn', 'Invalid create breakout room request for %s', orig_room.jid);
|
||||
return;
|
||||
end
|
||||
|
||||
local breakout_room_jid = uuid_gen() .. '@' .. breakout_rooms_muc_component_config;
|
||||
|
||||
if not main_room._data.breakout_rooms then
|
||||
@@ -219,13 +225,18 @@ function create_breakout_room(room_jid, subject)
|
||||
broadcast_breakout_rooms(main_room_jid);
|
||||
end
|
||||
|
||||
function destroy_breakout_room(room_jid, message)
|
||||
function destroy_breakout_room(orig_room, room_jid, message)
|
||||
local main_room, main_room_jid = get_main_room(room_jid);
|
||||
|
||||
if room_jid == main_room_jid then
|
||||
return;
|
||||
end
|
||||
|
||||
if orig_room ~= main_room then
|
||||
module:log('warn', 'Invalid destroy breakout room request for %s', orig_room.jid);
|
||||
return;
|
||||
end
|
||||
|
||||
local breakout_room = breakout_rooms_muc_service.get_room_from_jid(room_jid);
|
||||
|
||||
if breakout_room then
|
||||
@@ -244,13 +255,18 @@ function destroy_breakout_room(room_jid, message)
|
||||
end
|
||||
|
||||
|
||||
function rename_breakout_room(room_jid, name)
|
||||
function rename_breakout_room(orig_room, room_jid, name)
|
||||
local main_room, main_room_jid = get_main_room(room_jid);
|
||||
|
||||
if room_jid == main_room_jid then
|
||||
return;
|
||||
end
|
||||
|
||||
if orig_room ~= main_room then
|
||||
module:log('warn', 'Invalid rename breakout room request for %s', orig_room.jid);
|
||||
return;
|
||||
end
|
||||
|
||||
if main_room then
|
||||
if main_room._data.breakout_rooms then
|
||||
main_room._data.breakout_rooms[room_jid] = name;
|
||||
@@ -322,18 +338,25 @@ function on_message(event)
|
||||
end
|
||||
|
||||
if message.attr.type == JSON_TYPE_ADD_BREAKOUT_ROOM then
|
||||
create_breakout_room(room.jid, message.attr.subject);
|
||||
create_breakout_room(room, message.attr.subject);
|
||||
return true;
|
||||
elseif message.attr.type == JSON_TYPE_REMOVE_BREAKOUT_ROOM then
|
||||
destroy_breakout_room(message.attr.breakoutRoomJid);
|
||||
destroy_breakout_room(room, message.attr.breakoutRoomJid);
|
||||
return true;
|
||||
elseif message.attr.type == JSON_TYPE_RENAME_BREAKOUT_ROOM then
|
||||
rename_breakout_room(message.attr.breakoutRoomJid, message.attr.subject);
|
||||
rename_breakout_room(room, message.attr.breakoutRoomJid, message.attr.subject);
|
||||
return true;
|
||||
elseif message.attr.type == JSON_TYPE_MOVE_TO_ROOM_REQUEST then
|
||||
local participant_jid = message.attr.participantJid;
|
||||
local target_room_jid = message.attr.roomJid;
|
||||
|
||||
if not room._data.breakout_rooms or not (
|
||||
room._data.breakout_rooms[target_room_jid] or target_room_jid == internal_room_jid_match_rewrite(room.jid))
|
||||
then
|
||||
module:log('warn', 'Invalid breakout room %s for %s', target_room_jid, room.jid);
|
||||
return false
|
||||
end
|
||||
|
||||
local json_msg, error = json.encode({
|
||||
type = BREAKOUT_ROOMS_IDENTITY_TYPE,
|
||||
event = JSON_TYPE_MOVE_TO_ROOM_REQUEST,
|
||||
@@ -342,6 +365,7 @@ function on_message(event)
|
||||
|
||||
if not json_msg then
|
||||
module:log('error', 'skip sending request room:%s error:%s', room.jid, error);
|
||||
return false
|
||||
end
|
||||
|
||||
send_json_msg(participant_jid, json_msg)
|
||||
@@ -491,7 +515,7 @@ function on_main_room_destroyed(event)
|
||||
end
|
||||
|
||||
for breakout_room_jid in pairs(main_room._data.breakout_rooms or {}) do
|
||||
destroy_breakout_room(breakout_room_jid, event.reason)
|
||||
destroy_breakout_room(main_room, breakout_room_jid, event.reason)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
# If there is a tenant in the URL it must end with a slash (e.g. "https://alpha.jitsi.net/sometenant/")
|
||||
#BASE_URL=
|
||||
|
||||
# Room name suffix to use when creating new room names
|
||||
# ROOM_NAME_SUFFIX=
|
||||
|
||||
# To be able to match a domain to a specific address
|
||||
# The format is "MAP example.com 1.2.3.4"
|
||||
#RESOLVER_RULES=
|
||||
@@ -15,8 +18,8 @@
|
||||
# The path to the browser video capture file
|
||||
#VIDEO_CAPTURE_FILE=tests/resources/FourPeople_1280x720_30.y4m
|
||||
|
||||
# The path to the helper iframe page that will be used for the iframeAPI tests
|
||||
#IFRAME_PAGE_BASE=
|
||||
# The tenant used when executing the iframeAPI tests, will override any tenant from BASE_URL if any
|
||||
#IFRAME_TENANT=
|
||||
|
||||
# The grid host url (https://mygrid.com/wd/hub)
|
||||
#GRID_HOST_URL=
|
||||
@@ -26,6 +29,12 @@
|
||||
# The kid to use in the token
|
||||
#JWT_KID=
|
||||
|
||||
# An access token to use to create meetings (used for the first participant)
|
||||
#JWT_ACCESS_TOKEN=
|
||||
|
||||
# The count of workers that execute the tests in parallel
|
||||
# MAX_INSTANCES=1
|
||||
|
||||
# The address of the webhooks proxy used to test the webhooks feature (e.g. wss://your.service/?tenant=sometenant)
|
||||
#WEBHOOKS_PROXY_URL=
|
||||
# A shared secret to authenticate the webhook proxy connection
|
||||
|
||||
@@ -15,6 +15,7 @@ import LargeVideo from '../pageobjects/LargeVideo';
|
||||
import LobbyScreen from '../pageobjects/LobbyScreen';
|
||||
import Notifications from '../pageobjects/Notifications';
|
||||
import ParticipantsPane from '../pageobjects/ParticipantsPane';
|
||||
import PasswordDialog from '../pageobjects/PasswordDialog';
|
||||
import PreJoinScreen from '../pageobjects/PreJoinScreen';
|
||||
import SecurityDialog from '../pageobjects/SecurityDialog';
|
||||
import SettingsDialog from '../pageobjects/SettingsDialog';
|
||||
@@ -59,6 +60,30 @@ export class Participant {
|
||||
analytics: {
|
||||
disabled: true
|
||||
},
|
||||
|
||||
// if there is a video file to play, use deployment config,
|
||||
// otherwise use lower resolution to avoid high CPU usage
|
||||
constraints: process.env.VIDEO_CAPTURE_FILE ? undefined : {
|
||||
video: {
|
||||
height: {
|
||||
ideal: 360,
|
||||
max: 360,
|
||||
min: 180
|
||||
},
|
||||
|
||||
// @ts-ignore
|
||||
width: {
|
||||
ideal: 640,
|
||||
max: 640,
|
||||
min: 320
|
||||
},
|
||||
frameRate: {
|
||||
max: 30
|
||||
}
|
||||
}
|
||||
},
|
||||
resolution: process.env.VIDEO_CAPTURE_FILE ? undefined : 360,
|
||||
|
||||
requireDisplayName: false,
|
||||
testing: {
|
||||
testMode: true
|
||||
@@ -92,6 +117,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.
|
||||
*
|
||||
@@ -99,7 +142,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();
|
||||
});
|
||||
}
|
||||
@@ -176,7 +219,9 @@ export class Participant {
|
||||
// @ts-ignore
|
||||
url = `${this.driver.iframePageBase}${url}&domain="${baseUrl.host}"&room="${ctx.roomName}"`;
|
||||
|
||||
if (baseUrl.pathname.length > 1) {
|
||||
if (process.env.IFRAME_TENANT) {
|
||||
url = `${url}&tenant="${process.env.IFRAME_TENANT}"`;
|
||||
} else if (baseUrl.pathname.length > 1) {
|
||||
// remove leading slash
|
||||
url = `${url}&tenant="${baseUrl.pathname.substring(1)}"`;
|
||||
}
|
||||
@@ -187,8 +232,15 @@ export class Participant {
|
||||
|
||||
await this.driver.setTimeout({ 'pageLoad': 30000 });
|
||||
|
||||
let urlToLoad = url.startsWith('/') ? url.substring(1) : url;
|
||||
|
||||
if (options.forceGenerateToken && !ctx.iframeAPI && ctx.isJaasAvailable() && process.env.IFRAME_TENANT) {
|
||||
// This to enables tests like invite, which can force using the jaas auth instead of the provided token
|
||||
urlToLoad = `/${process.env.IFRAME_TENANT}/${urlToLoad}`;
|
||||
}
|
||||
|
||||
// drop the leading '/' so we can use the tenant if any
|
||||
await this.driver.url(url.startsWith('/') ? url.substring(1) : url);
|
||||
await this.driver.url(urlToLoad);
|
||||
|
||||
await this.waitForPageToLoad();
|
||||
|
||||
@@ -202,22 +254,21 @@ export class Participant {
|
||||
await this.waitToJoinMUC();
|
||||
}
|
||||
|
||||
await this.postLoadProcess(options.skipInMeetingChecks);
|
||||
await this.postLoadProcess();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads stuff after the page loads.
|
||||
*
|
||||
* @param {boolean} skipInMeetingChecks - Whether to skip in meeting checks.
|
||||
* @returns {Promise<void>}
|
||||
* @private
|
||||
*/
|
||||
private async postLoadProcess(skipInMeetingChecks = false): Promise<void> {
|
||||
private async postLoadProcess(): Promise<void> {
|
||||
const driver = this.driver;
|
||||
|
||||
const parallel = [];
|
||||
|
||||
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)
|
||||
@@ -242,15 +293,6 @@ export class Participant {
|
||||
}
|
||||
}, this._name, driver.sessionId, LOG_PREFIX));
|
||||
|
||||
if (skipInMeetingChecks) {
|
||||
await Promise.allSettled(parallel);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
parallel.push(this.waitForIceConnected());
|
||||
parallel.push(this.waitForSendReceiveData());
|
||||
|
||||
await Promise.all(parallel);
|
||||
}
|
||||
|
||||
@@ -261,7 +303,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}.`
|
||||
@@ -284,14 +326,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');
|
||||
}
|
||||
|
||||
@@ -299,7 +341,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());
|
||||
}
|
||||
|
||||
@@ -307,7 +349,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());
|
||||
}
|
||||
|
||||
@@ -331,16 +373,27 @@ 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}`
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for ICE to get connected on the p2p connection.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
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}`
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for send and receive data.
|
||||
*
|
||||
@@ -361,7 +414,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 = {
|
||||
@@ -382,11 +435,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}`
|
||||
});
|
||||
@@ -400,10 +453,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}`
|
||||
@@ -505,6 +556,13 @@ export class Participant {
|
||||
return new SettingsDialog(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the password dialog.
|
||||
*/
|
||||
getPasswordDialog(): PasswordDialog {
|
||||
return new PasswordDialog(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the prejoin screen.
|
||||
*/
|
||||
@@ -554,12 +612,18 @@ 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
|
||||
await this.driver.waitUntil(
|
||||
async () => current !== await this.driver.getUrl(),
|
||||
async () => {
|
||||
const u = await this.driver.getUrl();
|
||||
|
||||
console.log('url:', current, u);
|
||||
|
||||
return current !== u;
|
||||
},
|
||||
{
|
||||
timeout: 5000,
|
||||
timeoutMsg: `${this.name} did not leave the muc in 5s`
|
||||
@@ -695,7 +759,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;
|
||||
@@ -767,26 +831,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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -45,12 +45,31 @@ export async function ensureThreeParticipants(ctx: IContext, options: IJoinOptio
|
||||
})
|
||||
]);
|
||||
|
||||
const { skipInMeetingChecks } = options;
|
||||
if (options.skipInMeetingChecks) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
skipInMeetingChecks ? Promise.resolve() : ctx.p2.waitForRemoteStreams(2),
|
||||
skipInMeetingChecks ? Promise.resolve() : ctx.p3.waitForRemoteStreams(2)
|
||||
ctx.p1.waitForIceConnected(),
|
||||
ctx.p2.waitForIceConnected(),
|
||||
ctx.p3.waitForIceConnected()
|
||||
]);
|
||||
await Promise.all([
|
||||
ctx.p1.waitForSendReceiveData().then(() => ctx.p1.waitForRemoteStreams(1)),
|
||||
ctx.p2.waitForSendReceiveData().then(() => ctx.p2.waitForRemoteStreams(1)),
|
||||
ctx.p3.waitForSendReceiveData().then(() => ctx.p3.waitForRemoteStreams(1)),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -117,12 +136,21 @@ export async function ensureFourParticipants(ctx: IContext, options: IJoinOption
|
||||
})
|
||||
]);
|
||||
|
||||
const { skipInMeetingChecks } = options;
|
||||
if (options.skipInMeetingChecks) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
skipInMeetingChecks ? Promise.resolve() : ctx.p2.waitForRemoteStreams(3),
|
||||
skipInMeetingChecks ? Promise.resolve() : ctx.p3.waitForRemoteStreams(3),
|
||||
skipInMeetingChecks ? Promise.resolve() : ctx.p3.waitForRemoteStreams(3)
|
||||
ctx.p1.waitForIceConnected(),
|
||||
ctx.p2.waitForIceConnected(),
|
||||
ctx.p3.waitForIceConnected(),
|
||||
ctx.p4.waitForIceConnected()
|
||||
]);
|
||||
await Promise.all([
|
||||
ctx.p1.waitForSendReceiveData().then(() => ctx.p1.waitForRemoteStreams(1)),
|
||||
ctx.p2.waitForSendReceiveData().then(() => ctx.p2.waitForRemoteStreams(1)),
|
||||
ctx.p3.waitForSendReceiveData().then(() => ctx.p3.waitForRemoteStreams(1)),
|
||||
ctx.p4.waitForSendReceiveData().then(() => ctx.p4.waitForRemoteStreams(1)),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -137,9 +165,15 @@ async function joinTheModeratorAsP1(ctx: IContext, options?: IJoinOptions) {
|
||||
const p1DisplayName = P1_DISPLAY_NAME;
|
||||
let token;
|
||||
|
||||
// if it is jaas create the first one to be moderator and second not moderator
|
||||
if (ctx.jwtPrivateKeyPath && !options?.skipFirstModerator) {
|
||||
token = getModeratorToken(p1DisplayName);
|
||||
if (!options?.skipFirstModerator) {
|
||||
// we prioritize the access token when iframe is not used and private key is set,
|
||||
// otherwise if private key is not specified we use the access token if set
|
||||
if (process.env.JWT_ACCESS_TOKEN
|
||||
&& (ctx.jwtPrivateKeyPath && !ctx.iframeAPI && !options?.forceGenerateToken)) {
|
||||
token = process.env.JWT_ACCESS_TOKEN;
|
||||
} else if (ctx.jwtPrivateKeyPath) {
|
||||
token = getModeratorToken(p1DisplayName);
|
||||
}
|
||||
}
|
||||
|
||||
// make sure the first participant is moderator, if supported by deployment
|
||||
@@ -147,8 +181,7 @@ async function joinTheModeratorAsP1(ctx: IContext, options?: IJoinOptions) {
|
||||
ctx.p1 = p;
|
||||
}, {
|
||||
displayName: p1DisplayName,
|
||||
...options,
|
||||
skipInMeetingChecks: true
|
||||
...options
|
||||
}, token);
|
||||
}
|
||||
|
||||
@@ -161,17 +194,24 @@ async function joinTheModeratorAsP1(ctx: IContext, options?: IJoinOptions) {
|
||||
export async function ensureTwoParticipants(ctx: IContext, options: IJoinOptions = {}): Promise<void> {
|
||||
await joinTheModeratorAsP1(ctx, options);
|
||||
|
||||
const { skipInMeetingChecks } = options;
|
||||
await _joinParticipant('participant2', ctx.p2, p => {
|
||||
ctx.p2 = p;
|
||||
}, {
|
||||
displayName: P2_DISPLAY_NAME,
|
||||
...options
|
||||
});
|
||||
|
||||
if (options.skipInMeetingChecks) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
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)
|
||||
ctx.p1.waitForIceConnected(),
|
||||
ctx.p2.waitForIceConnected()
|
||||
]);
|
||||
await Promise.all([
|
||||
ctx.p1.waitForSendReceiveData().then(() => ctx.p1.waitForRemoteStreams(1)),
|
||||
ctx.p2.waitForSendReceiveData().then(() => ctx.p2.waitForRemoteStreams(1))
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -241,13 +281,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 +300,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.
|
||||
*/
|
||||
@@ -334,3 +388,23 @@ export async function checkSubject(participant: Participant, subject: string) {
|
||||
|
||||
expect(txt.startsWith(subject)).toBe(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a screensharing tile is displayed on the observer.
|
||||
* Expects there was already a video by this participant and screen sharing will be the second video `-v1`.
|
||||
*/
|
||||
export async function checkForScreensharingTile(sharer: Participant, observer: Participant, reverse = false) {
|
||||
await observer.driver.$(`//span[@id='participant_${await sharer.getEndpointId()}-v1']`).waitForDisplayed({
|
||||
timeout: 3_000,
|
||||
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()));
|
||||
}
|
||||
|
||||
@@ -7,12 +7,15 @@ export type IContext = {
|
||||
conferenceJid: string;
|
||||
dialInPin: string;
|
||||
iframeAPI: boolean;
|
||||
isJaasAvailable: () => boolean;
|
||||
jwtKid: string;
|
||||
jwtPrivateKeyPath: string;
|
||||
keepAlive: Array<any>;
|
||||
p1: Participant;
|
||||
p2: Participant;
|
||||
p3: Participant;
|
||||
p4: Participant;
|
||||
roomKey: string;
|
||||
roomName: string;
|
||||
skipSuiteTests: boolean;
|
||||
times: any;
|
||||
@@ -31,6 +34,12 @@ export type IJoinOptions = {
|
||||
*/
|
||||
displayName?: string;
|
||||
|
||||
/**
|
||||
* When joining the first participant and jwt singing material is available and a provided token
|
||||
* is available, prefer generating a new token for the first participant.
|
||||
*/
|
||||
forceGenerateToken?: boolean;
|
||||
|
||||
/**
|
||||
* Whether to skip setting display name.
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -136,11 +175,7 @@ export default class Filmstrip extends BasePageObject {
|
||||
* @param participant
|
||||
*/
|
||||
async muteAudio(participant: Participant) {
|
||||
const participantId = await participant.getEndpointId();
|
||||
|
||||
await this.participant.driver.$(`#participant-item-${participantId}`).moveTo();
|
||||
|
||||
await this.participant.driver.$(`button[data-testid="mute-audio-${participantId}"]`).click();
|
||||
await this.clickOnRemoteMenuLink(await participant.getEndpointId(), 'mutelink', false);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -159,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
|
||||
@@ -219,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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -25,6 +25,6 @@ export default class LobbyScreen extends PreMeetingScreen {
|
||||
* Waits for lobby screen to load.
|
||||
*/
|
||||
waitForLoading(): Promise<void> {
|
||||
return this.participant.driver.$('.lobby-screen').waitForDisplayed({ timeout: 4000 });
|
||||
return this.participant.driver.$('.lobby-screen').waitForDisplayed({ timeout: 6000 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
@@ -239,4 +238,16 @@ export default class ParticipantsPane extends BasePageObject {
|
||||
await rejectButton.waitForExist();
|
||||
await rejectButton.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mutes the audio of a participant.
|
||||
* @param participant
|
||||
*/
|
||||
async muteAudio(participant: Participant) {
|
||||
const participantId = await participant.getEndpointId();
|
||||
|
||||
await this.participant.driver.$(`#participant-item-${participantId}`).moveTo();
|
||||
|
||||
await this.participant.driver.$(`button[data-testid="mute-audio-${participantId}"]`).click();
|
||||
}
|
||||
}
|
||||
|
||||
38
tests/pageobjects/PasswordDialog.ts
Normal file
38
tests/pageobjects/PasswordDialog.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import BaseDialog from './BaseDialog';
|
||||
|
||||
const INPUT_KEY_XPATH = '//input[@name="lockKey"]';
|
||||
|
||||
/**
|
||||
* Represents the password dialog in a particular participant.
|
||||
*/
|
||||
export default class PasswordDialog extends BaseDialog {
|
||||
/**
|
||||
* Waiting for the dialog to appear.
|
||||
*/
|
||||
async waitForDialog() {
|
||||
const input = this.participant.driver.$(INPUT_KEY_XPATH);
|
||||
|
||||
await input.waitForExist({
|
||||
timeout: 5000,
|
||||
timeoutMsg: 'Password dialog not found'
|
||||
});
|
||||
await input.waitForDisplayed();
|
||||
await input.waitForStable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a password and submits the dialog.
|
||||
* @param password
|
||||
*/
|
||||
async submitPassword(password: string) {
|
||||
const passwordInput = this.participant.driver.$(INPUT_KEY_XPATH);
|
||||
|
||||
await passwordInput.waitForExist();
|
||||
await passwordInput.click();
|
||||
await passwordInput.clearValue();
|
||||
|
||||
await this.participant.driver.keys(password);
|
||||
|
||||
await this.clickOkButton();
|
||||
}
|
||||
}
|
||||
@@ -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}"]`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ export default abstract class PreMeetingScreen extends BasePageObject {
|
||||
return this.participant.driver.waitUntil(
|
||||
() => this.isLobbyRoomJoined(),
|
||||
{
|
||||
timeout: 3_000, // 3 seconds
|
||||
timeout: 6_000, // 6 seconds
|
||||
timeoutMsg: `Timeout waiting to join lobby for ${this.participant.name}`
|
||||
}
|
||||
);
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { Key } from 'webdriverio';
|
||||
|
||||
import BaseDialog from './BaseDialog';
|
||||
|
||||
const ADD_PASSWORD_LINK = 'add-password';
|
||||
@@ -7,6 +5,7 @@ const ADD_PASSWORD_FIELD = 'info-password-input';
|
||||
const DIALOG_CONTAINER = 'security-dialog';
|
||||
const LOCAL_LOCK = 'info-password-local';
|
||||
const REMOTE_LOCK = 'info-password-remote';
|
||||
const REMOVE_PASSWORD = 'remove-password';
|
||||
|
||||
/**
|
||||
* Page object for the security dialog.
|
||||
@@ -117,20 +116,19 @@ 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');
|
||||
/**
|
||||
* Removes the password from the current conference through the security dialog, if a password is set.
|
||||
*/
|
||||
async removePassword() {
|
||||
if (!await this.isLocked()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (validationMessage) {
|
||||
await this.participant.driver.keys([ Key.Escape ]);
|
||||
expect(validationMessage).toBe('');
|
||||
}
|
||||
const removePassword = this.participant.driver.$(`.${REMOVE_PASSWORD}`);
|
||||
|
||||
await removePassword.waitForClickable();
|
||||
await removePassword.click();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ensureTwoParticipants } from '../../helpers/participants';
|
||||
|
||||
describe('Audio only', () => {
|
||||
it('joining the meeting', () => ensureTwoParticipants(ctx, { skipFirstModerator: true }));
|
||||
it('joining the meeting', () => ensureTwoParticipants(ctx));
|
||||
|
||||
/**
|
||||
* Enables audio only mode for p1 and verifies that the other participant sees participant1 as video muted.
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
42
tests/specs/2way/grantModerator.spec.ts
Normal file
42
tests/specs/2way/grantModerator.spec.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { ensureOneParticipant, ensureTwoParticipants } from '../../helpers/participants';
|
||||
|
||||
describe('Grant moderator', () => {
|
||||
it('joining the meeting', async () => {
|
||||
await ensureOneParticipant(ctx);
|
||||
|
||||
if (await ctx.p1.execute(() => typeof APP.conference._room.grantOwner !== 'function')) {
|
||||
ctx.skipSuiteTests = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await ensureTwoParticipants(ctx);
|
||||
});
|
||||
|
||||
it('grant moderator and validate', async () => {
|
||||
const { p1, p2 } = ctx;
|
||||
|
||||
if (!await p1.isModerator()) {
|
||||
ctx.skipSuiteTests = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (await p2.isModerator()) {
|
||||
ctx.skipSuiteTests = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await p1.getFilmstrip().grantModerator(p2);
|
||||
|
||||
await p2.driver.waitUntil(
|
||||
() => p2.isModerator(),
|
||||
{
|
||||
timeout: 3000,
|
||||
timeoutMsg: 'p2 did not become moderator'
|
||||
}
|
||||
);
|
||||
|
||||
});
|
||||
});
|
||||
@@ -37,6 +37,13 @@ describe('Participants presence', () => {
|
||||
|
||||
const { p1, p2, webhooksProxy } = ctx;
|
||||
|
||||
if (await p1.execute(() => config.disableIframeAPI)) {
|
||||
// skip the test if iframeAPI is disabled
|
||||
ctx.skipSuiteTests = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// let's populate endpoint ids
|
||||
await Promise.all([
|
||||
p1.getEndpointId(),
|
||||
|
||||
181
tests/specs/2way/lockRoom.spec.ts
Normal file
181
tests/specs/2way/lockRoom.spec.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
import { ensureOneParticipant, ensureTwoParticipants, joinSecondParticipant } from '../../helpers/participants';
|
||||
import type SecurityDialog from '../../pageobjects/SecurityDialog';
|
||||
|
||||
/**
|
||||
* 1. Lock the room (make sure the image changes to locked)
|
||||
* 2. Join with a second browser/tab
|
||||
* 3. Make sure we are required to enter a password.
|
||||
* (Also make sure the padlock is locked)
|
||||
* 4. Enter wrong password, make sure we are not joined in the room
|
||||
* 5. Unlock the room (Make sure the padlock is unlocked)
|
||||
* 6. Join again and make sure we are not asked for a password and that
|
||||
* the padlock is unlocked.
|
||||
*/
|
||||
describe('Lock Room', () => {
|
||||
it('joining the meeting', () => ensureOneParticipant(ctx));
|
||||
|
||||
it('locks the room', () => participant1LockRoom());
|
||||
|
||||
it('enter participant in locked room', async () => {
|
||||
// first enter wrong pin then correct one
|
||||
await joinSecondParticipant(ctx, {
|
||||
skipWaitToJoin: true,
|
||||
skipInMeetingChecks: true
|
||||
});
|
||||
|
||||
const { p2 } = ctx;
|
||||
|
||||
// wait for password prompt
|
||||
const p2PasswordDialog = p2.getPasswordDialog();
|
||||
|
||||
await p2PasswordDialog.waitForDialog();
|
||||
await p2PasswordDialog.submitPassword(`${ctx.roomKey}1234`);
|
||||
|
||||
// wait for password prompt
|
||||
await p2PasswordDialog.waitForDialog();
|
||||
await p2PasswordDialog.submitPassword(ctx.roomKey);
|
||||
|
||||
await p2.waitToJoinMUC();
|
||||
|
||||
const p2SecurityDialog = p2.getSecurityDialog();
|
||||
|
||||
await p2.getToolbar().clickSecurityButton();
|
||||
await p2SecurityDialog.waitForDisplay();
|
||||
|
||||
await waitForRoomLockState(p2SecurityDialog, true);
|
||||
});
|
||||
|
||||
it('unlock room', async () => {
|
||||
// Unlock room. Check whether room is still locked. Click remove and check whether it is unlocked.
|
||||
await ctx.p2.hangup();
|
||||
|
||||
await participant1UnlockRoom();
|
||||
});
|
||||
|
||||
it('enter participant in unlocked room', async () => {
|
||||
// Just enter the room and check that is not locked.
|
||||
// if we fail to unlock the room this one will detect it
|
||||
// as participant will fail joining
|
||||
await ensureTwoParticipants(ctx);
|
||||
|
||||
const { p2 } = ctx;
|
||||
const p2SecurityDialog = p2.getSecurityDialog();
|
||||
|
||||
await p2.getToolbar().clickSecurityButton();
|
||||
await p2SecurityDialog.waitForDisplay();
|
||||
|
||||
await waitForRoomLockState(p2SecurityDialog, false);
|
||||
|
||||
await p2SecurityDialog.clickCloseButton();
|
||||
});
|
||||
|
||||
it('update locked state while participants in room', async () => {
|
||||
// Both participants are in unlocked room, lock it and see whether the
|
||||
// change is reflected on the second participant icon.
|
||||
await participant1LockRoom();
|
||||
|
||||
const { p2 } = ctx;
|
||||
const p2SecurityDialog = p2.getSecurityDialog();
|
||||
|
||||
await p2.getToolbar().clickSecurityButton();
|
||||
await p2SecurityDialog.waitForDisplay();
|
||||
|
||||
await waitForRoomLockState(p2SecurityDialog, true);
|
||||
|
||||
await participant1UnlockRoom();
|
||||
|
||||
await waitForRoomLockState(p2SecurityDialog, false);
|
||||
});
|
||||
it('unlock after participant enter wrong password', async () => {
|
||||
// P1 locks the room. Participant tries to enter using wrong password.
|
||||
// P1 unlocks the room and Participant submits the password prompt with no password entered and
|
||||
// should enter of unlocked room.
|
||||
await ctx.p2.hangup();
|
||||
await participant1LockRoom();
|
||||
await joinSecondParticipant(ctx, {
|
||||
skipWaitToJoin: true,
|
||||
skipInMeetingChecks: true
|
||||
});
|
||||
|
||||
const { p2 } = ctx;
|
||||
|
||||
// wait for password prompt
|
||||
const p2PasswordDialog = p2.getPasswordDialog();
|
||||
|
||||
await p2PasswordDialog.waitForDialog();
|
||||
await p2PasswordDialog.submitPassword(`${ctx.roomKey}1234`);
|
||||
|
||||
// wait for password prompt
|
||||
await p2PasswordDialog.waitForDialog();
|
||||
|
||||
await participant1UnlockRoom();
|
||||
|
||||
await p2PasswordDialog.clickOkButton();
|
||||
await p2.waitToJoinMUC();
|
||||
|
||||
const p2SecurityDialog = p2.getSecurityDialog();
|
||||
|
||||
await p2.getToolbar().clickSecurityButton();
|
||||
await p2SecurityDialog.waitForDisplay();
|
||||
|
||||
await waitForRoomLockState(p2SecurityDialog, false);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Participant1 locks the room.
|
||||
*/
|
||||
async function participant1LockRoom() {
|
||||
ctx.roomKey = `${Math.trunc(Math.random() * 1_000_000)}`;
|
||||
|
||||
const { p1 } = ctx;
|
||||
const p1SecurityDialog = p1.getSecurityDialog();
|
||||
|
||||
await p1.getToolbar().clickSecurityButton();
|
||||
await p1SecurityDialog.waitForDisplay();
|
||||
|
||||
await waitForRoomLockState(p1SecurityDialog, false);
|
||||
|
||||
await p1SecurityDialog.addPassword(ctx.roomKey);
|
||||
|
||||
await p1SecurityDialog.clickCloseButton();
|
||||
|
||||
await p1.getToolbar().clickSecurityButton();
|
||||
await p1SecurityDialog.waitForDisplay();
|
||||
|
||||
await waitForRoomLockState(p1SecurityDialog, true);
|
||||
|
||||
await p1SecurityDialog.clickCloseButton();
|
||||
}
|
||||
|
||||
/**
|
||||
* Participant1 unlocks the room.
|
||||
*/
|
||||
async function participant1UnlockRoom() {
|
||||
const { p1 } = ctx;
|
||||
const p1SecurityDialog = p1.getSecurityDialog();
|
||||
|
||||
await p1.getToolbar().clickSecurityButton();
|
||||
await p1SecurityDialog.waitForDisplay();
|
||||
|
||||
await p1SecurityDialog.removePassword();
|
||||
|
||||
await waitForRoomLockState(p1SecurityDialog, false);
|
||||
|
||||
await p1SecurityDialog.clickCloseButton();
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for the room to be locked or unlocked.
|
||||
* @param securityDialog
|
||||
* @param locked
|
||||
*/
|
||||
function waitForRoomLockState(securityDialog: SecurityDialog, locked: boolean) {
|
||||
return securityDialog.participant.driver.waitUntil(
|
||||
async () => await securityDialog.isLocked() === locked,
|
||||
{
|
||||
timeout: 3_000, // 3 seconds
|
||||
timeoutMsg: `Timeout waiting for the room to unlock for ${securityDialog.participant.name}.`
|
||||
}
|
||||
);
|
||||
}
|
||||
138
tests/specs/2way/mute.spect.ts
Normal file
138
tests/specs/2way/mute.spect.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import type { Participant } from '../../helpers/Participant';
|
||||
import {
|
||||
checkForScreensharingTile,
|
||||
ensureOneParticipant,
|
||||
ensureTwoParticipants,
|
||||
joinSecondParticipant,
|
||||
muteAudioAndCheck,
|
||||
unmuteAudioAndCheck,
|
||||
unmuteVideoAndCheck
|
||||
} from '../../helpers/participants';
|
||||
|
||||
describe('Mute', () => {
|
||||
it('joining the meeting', () => ensureTwoParticipants(ctx));
|
||||
|
||||
it('mute p1 and check', () => toggleMuteAndCheck(ctx.p1, ctx.p2, true));
|
||||
|
||||
it('unmute p1 and check', () => toggleMuteAndCheck(ctx.p1, ctx.p2, false));
|
||||
|
||||
it('mute p2 and check', () => toggleMuteAndCheck(ctx.p2, ctx.p1, true));
|
||||
|
||||
it('unmute p2 and check', () => toggleMuteAndCheck(ctx.p2, ctx.p1, false));
|
||||
|
||||
it('p1 mutes p2 and check', async () => {
|
||||
const { p1, p2 } = ctx;
|
||||
|
||||
if (!await p1.isModerator()) {
|
||||
return;
|
||||
}
|
||||
|
||||
await p1.getFilmstrip().muteAudio(p2);
|
||||
|
||||
// and now check whether second participant is muted
|
||||
await p2.getFilmstrip().assertAudioMuteIconIsDisplayed(p2);
|
||||
});
|
||||
|
||||
it('p2 unmute after p1 mute and check', async () => {
|
||||
const { p1, p2 } = ctx;
|
||||
|
||||
await unmuteAudioAndCheck(p2, p1);
|
||||
});
|
||||
|
||||
it('p1 mutes before p2 joins', async () => {
|
||||
await ctx.p2.hangup();
|
||||
|
||||
const { p1 } = ctx;
|
||||
|
||||
await p1.getToolbar().clickAudioMuteButton();
|
||||
|
||||
await ensureTwoParticipants(ctx);
|
||||
|
||||
const { p2 } = ctx;
|
||||
|
||||
await p2.getFilmstrip().assertAudioMuteIconIsDisplayed(p1);
|
||||
|
||||
await toggleMuteAndCheck(p1, p2, false);
|
||||
});
|
||||
|
||||
it('mute before join and screen share after in p2p', () => muteP1BeforeP2JoinsAndScreenshare(true));
|
||||
|
||||
it('mute before join and screen share after with jvb', () => muteP1BeforeP2JoinsAndScreenshare(false));
|
||||
});
|
||||
|
||||
/**
|
||||
* Toggles the mute state of a specific Meet conference participant and
|
||||
* verifies that a specific other Meet conference participants sees a
|
||||
* specific mute state for the former.
|
||||
* @param testee The participant whose mute state is to be toggled.
|
||||
* @param observer The participant to verify the mute state of {@code testee}.
|
||||
* @param muted the mute state of {@code testee} expected to be observed by {@code observer}.
|
||||
*/
|
||||
async function toggleMuteAndCheck(
|
||||
testee: Participant,
|
||||
observer: Participant,
|
||||
muted: boolean) {
|
||||
if (muted) {
|
||||
await muteAudioAndCheck(testee, observer);
|
||||
} else {
|
||||
await unmuteAudioAndCheck(testee, observer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Video mutes participant1 before participant2 joins and checks if participant1 can share or unmute video
|
||||
* and that media is being received on participant2 in both the cases.
|
||||
*
|
||||
* @param p2p whether to enable p2p or not.
|
||||
*/
|
||||
async function muteP1BeforeP2JoinsAndScreenshare(p2p: boolean) {
|
||||
await Promise.all([ ctx.p1?.hangup(), ctx.p2?.hangup() ]);
|
||||
|
||||
await ensureOneParticipant(ctx, {
|
||||
configOverwrite: {
|
||||
p2p: {
|
||||
enabled: p2p
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const { p1 } = ctx;
|
||||
|
||||
await p1.getToolbar().clickVideoMuteButton();
|
||||
|
||||
await joinSecondParticipant(ctx, {
|
||||
configOverwrite: {
|
||||
p2p: {
|
||||
enabled: p2p
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const { p2 } = ctx;
|
||||
|
||||
if (p2p) {
|
||||
await p2.waitForP2PIceConnected();
|
||||
} else {
|
||||
await p2.waitForIceConnected();
|
||||
}
|
||||
|
||||
await p2.waitForSendReceiveData({ checkReceive: false });
|
||||
|
||||
// Check if p1 appears video muted on p2.
|
||||
await p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1);
|
||||
|
||||
// Start desktop share.
|
||||
await p1.getToolbar().clickDesktopSharingButton();
|
||||
|
||||
await checkForScreensharingTile(p1, p2);
|
||||
|
||||
// we need to pass the id of the fake participant we use for the screensharing
|
||||
await p2.waitForRemoteVideo(`${await p1.getEndpointId()}-v1`);
|
||||
|
||||
// Stop desktop share and unmute video and check for video again.
|
||||
await p1.getToolbar().clickStopDesktopSharingButton();
|
||||
|
||||
await p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1);
|
||||
await unmuteVideoAndCheck(p1, p2);
|
||||
await p2.waitForRemoteVideo(await p1.getEndpointId());
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user