Compare commits

...

147 Commits

Author SHA1 Message Date
Saúl Ibarra Corretgé
014f3b615f fix(android) increase gradle JVM heap size 2020-09-29 17:35:35 +02:00
Jaya Allamsetty
25271d7eec fix(presenter): Do not change the video mute state on presenter mute.
This fixes the issue where the local preview appears muted when presenter camera is turned on and then off while screenshare is in progress.
2020-09-29 08:35:42 -04:00
Saúl Ibarra Corretgé
7ef4de9c1c chore(deps,android) bump leakcanary 2020-09-29 14:32:41 +02:00
Saúl Ibarra Corretgé
e6e088d197 fix(crashlytics) add missing dependency
Looks like the "Firebase Analytics" dependency is needed when migrating to the
new Firebase Crashlytics SDK. We are only interested in the "latest iversion
crash-free users" stat, which seems to require this. The documentartion is
somewhat confusing though.
2020-09-29 14:32:41 +02:00
Andrei Gavrilescu
0e034a686f deps: lib-jitsi-meet support rtcstats p2p meta 2020-09-29 15:18:23 +03:00
Saúl Ibarra Corretgé
d9f85c70f1 chore(deps,rn) react-native-webview@10.9.0 2020-09-29 14:16:36 +02:00
Titus-Andrei Moldovan
de8079cc98 fix(android) update Gradle and the plugin to the latest versions 2020-09-29 14:16:36 +02:00
Titus-Andrei Moldovan
2a9805f9b1 feat(android) revert to JSC as our JS engine
JSC wasn't the cause for the crash we were hunting after all. RN doesn't set
HErmes as the default, neither does Expo, so the jury is still out on Hermes,
and it looks like JSC is still the safest bet.

In addition, the way Hermes is packaged (as a standalone AARs, instead of a
local "Maven repo") complicates the SDK build and can make the resulting build
bloated.
2020-09-29 14:16:36 +02:00
Saúl Ibarra Corretgé
00ec0f03a6 chore(deps) run npm audit fix 2020-09-29 13:51:07 +02:00
Дамян Минков
91f636a813 debian: Don't break those still using ALPN multiplex (Fixes #7794) (#7796)
* fix: Don't break those still using ALPN multiplex (Fixes #7794)

* squash: Update doc/debian/jitsi-meet/jitsi-meet.conf

Co-authored-by: Saúl Ibarra Corretgé <saghul@jitsi.org>

Co-authored-by: Saúl Ibarra Corretgé <saghul@jitsi.org>
2020-09-29 06:17:42 -05:00
roms2000
fa4df19733 fix(lang) update French translation 2020-09-29 10:22:50 +02:00
DecaTec
1d17cc91e0 fix(lang) typo in German translation 2020-09-29 10:22:05 +02:00
Дамян Минков
93f4098dc0 feat: Enable bridge websockets by default for new installs (#7781)
* feat: Drops multiplexing support by default.

* fix: Fix purge of jitsi-meet-prosody.

Clean the accounts when there is a - in the domain name.
Removes the certificate so reinstall will not cause problems.

* feat: Enables bridge websockets by default.

* fix: External-ip conflicts with denied-peer-ip.

In cases where the bridge and coturn are on the same machine and the local address is any of the networks from denied-peer-ip, coturn is not using its public address to probe it and communication fails as the other address is deneid.

* squash: Fix a comment.
2020-09-25 13:15:58 -05:00
Saúl Ibarra Corretgé
11ae187ece fix(chat) prevent homograph attacks
Decode URLs using punycode when rendering, so when http://ebаy.com is sent
we render http://xn--eby-7cd.com/ instead.

Ref: https://github.com/tasti/react-linkify/issues/84
2020-09-25 12:46:29 +02:00
Jaya Allamsetty
0f9e01a7cf feat(iFrame): Add method for pinning a participant on stage view 2020-09-24 21:42:58 -04:00
Saúl Ibarra Corretgé
ddbd3f292a fix(analytics) clarify log line 2020-09-24 13:39:41 +02:00
Saúl Ibarra Corretgé
b153bf2fb8 fix(analytics) make handler loading more resilient
- Don't initialize handler's is their API key is not set
- Don't swallow exceptions when creating handlers
- Don't remove all handlers if an external one fails
- Dispose the analytics subsystem if no handlers are registered
2020-09-24 13:39:41 +02:00
Saúl Ibarra Corretgé
919be21912 fix(analytics) make sure rtcstats is not enabled on mobile 2020-09-24 13:39:41 +02:00
Saúl Ibarra Corretgé
1a339100ab fix(analytics) avoid Amplitude initialization failure on mobile 2020-09-24 13:39:41 +02:00
Tudor-Ovidiu Avram
ce4ef96941 fix(vpaas) hide embed meeting for vpaas users 2020-09-24 12:06:51 +02:00
Tudor-Ovidiu Avram
993ded9936 fix(vpaas) fix vpaas redirect 2020-09-24 12:06:28 +02:00
Jaya Allamsetty
a8b9ae2b12 fix(callstats): Use callStatsThreshold for % of users instead of conferences 2020-09-23 18:58:38 -04:00
paweldomas
812af33a4d fix(mediaDeviceHelper): fix typo in dispatch 2020-09-23 12:57:53 -04:00
paweldomas
7f17c2eceb fix(LargeVideoManager): large video resizing
Distinguish between preferred and calculated width/height
values.
2020-09-23 12:54:26 -04:00
Jaya Allamsetty
09124ad7e9 fix(iframe): Use largeVideo video element for screenshot.
Get the existing HTMLVideoElement for large video instead of creating a new video element for capturing the screenshot.
This should prevent the video player from getting displayed on mobile Safari.
2020-09-23 10:35:36 -04:00
Jaya Allamsetty
7a9a6855b7 fix(UI): Re-compute large-video width only once if the chat window is open.
Deduct the chat window width from large-video width only once if we keep toggling between stage view and grid view while the chat window is open.
2020-09-22 17:44:49 -04:00
Дамян Минков
8dcf04897a feat: Throttle out call attempts to the max number per minute (#7742)
* feat: Make possible to reload config for filter rayo iq.

* feat: Throttle out call attempts to the max number per minute

* squash: Updates comment about config
2020-09-22 10:53:43 -05:00
Saúl Ibarra Corretgé
69b7301b9d fix(build) reduce bundle size by about 700K
app.bundle.js before: 3851549 after: 4506493.

The culprit for the bloat was Olm. It feature-detects the environment in order
to pick a suitable random byte generator, and alas Webpack includes the None
crypto pollyfill. This is due to the existence of the "node" block in our
Webpack configuration file.

The solution is to provide empty modules to make bundling work, as we did
already for the fs module, since we know they are not used at runtime.
2020-09-22 10:59:55 +02:00
Jaya Allamsetty
794713b930 fix(iFrame): Make resizeLargeVideo only available on web 2020-09-21 19:54:45 -04:00
Jaya Allamsetty
89cd6e8e3e feat(stats): Add the ability to enable callStats for certain % of confs 2020-09-21 13:20:01 -04:00
Saúl Ibarra Corretgé
7a7937c072 fix(ios,version) versions must match 2020-09-18 21:02:51 +02:00
Saúl Ibarra Corretgé
4765ab9d63 chore(rn,version) bump app and sdk versions 2020-09-18 17:55:41 +02:00
Jaya Allamsetty
1d5decc14f feat(iFrame): Add a method for capturing screenshot of the large video (#7717) 2020-09-18 11:53:27 -04:00
Saúl Ibarra Corretgé
119b79fd84 fix(SmallVideo) screen-sharing indicator
The stream is attached before the video type change event is fired, so comparing
them is too late. Unconditionally update the screen-sharing indicator, and
perform the check for a change right there, to avoid re-renders.
2020-09-18 17:17:30 +02:00
Saúl Ibarra Corretgé
188771751d fix(config) remove unused options (#7723)
* fix(config) remove unused options

* Update configWhitelist.js
2020-09-18 08:55:09 -05:00
Vlad Piersec
d2ec0ea6f3 fix(branding): Fix dynamic logo display
* Display of the logo has been reworked (simplified).
* The logo will not be displayed if the call to `branding` endpoint fails.
* Add more docs.
2020-09-18 16:38:44 +03:00
Saúl Ibarra Corretgé
ed6e75b241 fix(ios) add local network usage description for iOS 14 2020-09-18 12:54:11 +02:00
Saúl Ibarra Corretgé
dedd3f4ef0 fix(config) remove no longer valid option 2020-09-18 12:31:55 +02:00
Jaya Allamsetty
bbb4fbd5f8 feat(iFrame): Add a method for resizing large video container from iFrame 2020-09-17 23:41:19 -04:00
Saúl Ibarra Corretgé
92235ae535 fix(android,calendar) avoid crash 2020-09-17 18:45:04 +02:00
Josh Brown
ebb1b8d76b fix(background-blur) refactor to improve performance 2020-09-17 18:25:06 +02:00
Saúl Ibarra Corretgé
42d559de93 deps: replace node-sass with sass
The former is no longer actively maintained.

Fixes: https://github.com/jitsi/jitsi-meet/issues/6427
2020-09-17 16:44:01 +02:00
Julian Vos
2838aefccc lang: fix Dutch dialog.kickTitle 2020-09-17 16:29:18 +02:00
Saúl Ibarra Corretgé
ca306f47b6 deps: react-native-background-timer@2.4.0
We hadn't updated in a while.
2020-09-17 13:15:04 +02:00
Saúl Ibarra Corretgé
56da400f19 ios: stop using react-native-background-timer
Ever since https://github.com/facebook/react-native/pull/23674 landed it has
been possible to run timers in the background, assuming your app is allowed to
run in the background already, as is our case. So, stop using the library on
iOS, which will avoid creatring needless backgound tasks.
2020-09-17 13:15:04 +02:00
Manuel Garcia
ab21e3cd5e fix(embed): remove legacy attribute from embed meeting code 2020-09-17 09:11:41 +02:00
damencho
2c026754ef fix: Fix ws reconnect piling up previd param. 2020-09-16 16:43:48 -05:00
Jaya Allamsetty
8dbe3e37b9 feat(iFrame): add a method for getting the participants info 2020-09-16 15:03:47 -04:00
Tudor-Ovidiu Avram
7f67f78db6 fix(embed) fix embed meeting code 2020-09-16 06:59:41 -05:00
Saúl Ibarra Corretgé
312949eef6 ios: update Crashlytics dependency 2020-09-15 21:22:50 +02:00
Saúl Ibarra Corretgé
41ea94c0c2 android: update AndroidX core library dependencies 2020-09-15 21:22:50 +02:00
Saúl Ibarra Corretgé
e70adef2ef android: update Crashlytics dependency 2020-09-15 21:22:50 +02:00
Saúl Ibarra Corretgé
57bbe3f75a android: fix crash when requesting permissions
The RN Permissions module calls this in a non-UI thread. What we observe is a
crash in ViewGroup.dispatchCancelPendingInputEvents, which is called on the
calling (ie, non-UI) thread. This doesn't look very safe, so try to avoid a
crash by pretending the permission was denied.
2020-09-15 16:17:46 +02:00
Tudor-Ovidiu Avram
e2731ce73e feat(loggin) forward logs to external api 2020-09-15 09:44:50 +02:00
yjhgull
d5dae945a8 lang: update Korean translation 2020-09-15 09:33:35 +02:00
Jaya Allamsetty
4d1dba937f feat(external_api): Add method for displaying participant on large video 2020-09-14 19:39:19 -04:00
Jaya Allamsetty
b6792db65f feat(external_api): Add cmd for selecting a user to be displayed in large video 2020-09-14 19:39:19 -04:00
Saúl Ibarra Corretgé
9815b633fc deps: lib-jitsi-meet@latest
Avoids CORS issues with the E2EE worker.
2020-09-11 16:13:12 +02:00
Joris Bodin
b4bf82429c lang: update French translation 2020-09-11 10:07:41 +02:00
Aaron van Meerten
53d485b397 Merge pull request #7679 from jitsi/mod-token-update
fix: Updates docs and verification to halt joining process.
2020-09-10 12:46:28 -05:00
damencho
0354dbe889 fix: Updates docs and verification to halt joining process.
When returning the error and showing to user not allowed screen we were not completely halting the prejoin operation when token verification fails on room join and the token is valid in general.
2020-09-10 10:07:30 -05:00
Saúl Ibarra Corretgé
7cafa205ee e2ee: stage 2
Adapt to E2EE changes in lib-jitsi-meet. Notably:

---
    e2ee: introduce per-participant randomly generated keys

    This the second stage in our E2EE journey.

    Instead of using a single pre-shared passphrase for deriving the key used for
    E2EE, we now establish a secure E2EE communication channel amongst peers.

    This channel is implemented using libolm, using XMPP groupchat or JVB channels
    as the transport.

    Once the secure E2EE channel has been established each participant will generate
    a random 32 byte key and exchange it over this channel.

    Keys are rotated (well, just re-created at the moment) when a participant joins
    or leaves.
---
2020-09-10 16:06:25 +02:00
Saúl Ibarra Corretgé
2b4f33bef8 e2ee: use a separate bundle for the worker 2020-09-10 16:06:25 +02:00
Selyan Slimane Amiri
31dee0bb68 lang: update kabyle translations 2020-09-10 10:30:09 +02:00
Hristo Terezov
fc75d45c6c feat(video-quality): add iframe event and getter. 2020-09-09 18:04:17 -05:00
Hristo Terezov
25839b18d2 feat(video-quality): persist. 2020-09-09 18:04:17 -05:00
Hristo Terezov
43f36c8cfd fix(ifarme-api): set-video-quality to use redux. 2020-09-09 18:04:17 -05:00
Hristo Terezov
b02d96231c ref(video-quality): Move all related code. 2020-09-09 18:04:17 -05:00
Дамян Минков
651d713206 feat: Allows jvb to control DTLS/SRTP protection profile. (#7626)
* feat: Allows jvb to control DTLS/SRTP protection profile.

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

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

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

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

* - Adding config flag to enable the feature
2020-08-30 09:36:52 -05:00
Hristo Terezov
5e85b5f63a fix(close3): Add close3.js 2020-08-28 11:33:19 -05:00
vp8x8
74f7c4141f fix(vpaas): Fix billing counter auth (#7595) 2020-08-28 15:43:14 +03:00
Vlad Piersec
4866ddc2ad fix(vpaas): Fix tenant typo 2020-08-28 11:08:59 +03:00
Vlad Piersec
71d0577a49 feat(vpaas): Add endpoint counter & remove branding on vpaas meetings 2020-08-27 14:49:03 -05:00
Hristo Terezov
b7529863d5 fix(iframe-api): setDevice. 2020-08-25 18:37:03 -05:00
Hristo Terezov
4ded94d130 fix(settings): store url display name and email. 2020-08-25 18:37:03 -05:00
Jaya Allamsetty
eb8b730227 deps: update lib-jitsi-meet to latest.
Update config.js to include the new codec preference options under videoQuality settings.
2020-08-25 16:52:48 -04:00
Vlad Piersec
4bd57692b7 feat(prejoin): Show warning if audio device does not receive data 2020-08-25 11:39:59 -05:00
Aaron van Meerten
5d012c24a7 Merge pull request #7508 from abora8x8/abora/vpass
Add pre and post validation for users that want to use their own publ…
2020-08-24 09:45:21 -05:00
Vlad Piersec
4f52a29120 fix(prejoin): Make avatar resizable 2020-08-21 14:10:24 -05:00
Tudor-Ovidiu Avram
8a4fb72eae feat(branding) allow invite links to be branded 2020-08-21 11:00:12 -05:00
paweldomas
6453ceb048 ref: remove jest and lastn functions.test.js
It doesn't play well with webpack and it's babel config
and I couldn't find a way to make it work.
2020-08-21 07:38:21 -07:00
Andrei Gavrilescu
e51bbe6125 fix syntax error 2020-08-20 17:30:59 -05:00
Andrei Gavrilescu
d725c0ab8a Use rtcstats with keep-alive / add rtcstats enabled config 2020-08-20 17:30:59 -05:00
Hristo Terezov
2c2edace2a Merge pull request #7475 from vp8x8/prejoin-focus
fix(prejoin): Auto focus display name input
2020-08-20 15:28:04 -05:00
paweldomas
d3d5847605 feat: configurable quality levels for video height
Allows to adjust thresholds which control the video quality level
in the thumbnail view.

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

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

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

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

1
.gitignore vendored
View File

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

View File

@@ -3,8 +3,9 @@ CLEANCSS = ./node_modules/.bin/cleancss
DEPLOY_DIR = libs
LIBJITSIMEET_DIR = node_modules/lib-jitsi-meet/
LIBFLAC_DIR = node_modules/libflacjs/dist/min/
OLM_DIR = node_modules/olm
RNNOISE_WASM_DIR = node_modules/rnnoise-wasm/dist/
NODE_SASS = ./node_modules/.bin/node-sass
NODE_SASS = ./node_modules/.bin/sass
NPM = npm
OUTPUT_DIR = .
STYLES_BUNDLE = css/all.bundle.css
@@ -22,7 +23,7 @@ clean:
rm -fr $(BUILD_DIR)
.NOTPARALLEL:
deploy: deploy-init deploy-appbundle deploy-rnnoise-binary deploy-lib-jitsi-meet deploy-libflac deploy-css deploy-local
deploy: deploy-init deploy-appbundle deploy-rnnoise-binary deploy-lib-jitsi-meet deploy-libflac deploy-olm deploy-css deploy-local
deploy-init:
rm -fr $(DEPLOY_DIR)
@@ -51,12 +52,15 @@ deploy-appbundle:
$(BUILD_DIR)/video-blur-effect.min.map \
$(BUILD_DIR)/rnnoise-processor.min.js \
$(BUILD_DIR)/rnnoise-processor.min.map \
$(BUILD_DIR)/close3.min.js \
$(BUILD_DIR)/close3.min.map \
$(DEPLOY_DIR)
deploy-lib-jitsi-meet:
cp \
$(LIBJITSIMEET_DIR)/lib-jitsi-meet.min.js \
$(LIBJITSIMEET_DIR)/lib-jitsi-meet.min.map \
$(LIBJITSIMEET_DIR)/lib-jitsi-meet.e2ee-worker.js \
$(LIBJITSIMEET_DIR)/connection_optimization/external_connect.js \
$(LIBJITSIMEET_DIR)/modules/browser/capabilities.json \
$(DEPLOY_DIR)
@@ -67,6 +71,11 @@ deploy-libflac:
$(LIBFLAC_DIR)/libflac4-1.3.2.min.js.mem \
$(DEPLOY_DIR)
deploy-olm:
cp \
$(OLM_DIR)/olm.wasm \
$(DEPLOY_DIR)
deploy-rnnoise-binary:
cp \
$(RNNOISE_WASM_DIR)/rnnoise.wasm \
@@ -81,7 +90,7 @@ deploy-local:
([ ! -x deploy-local.sh ] || ./deploy-local.sh)
.NOTPARALLEL:
dev: deploy-init deploy-css deploy-rnnoise-binary deploy-lib-jitsi-meet deploy-libflac
dev: deploy-init deploy-css deploy-rnnoise-binary deploy-lib-jitsi-meet deploy-libflac deploy-olm
$(WEBPACK_DEV_SERVER) --detect-circular-deps
source-package:

View File

@@ -3,7 +3,7 @@ apply plugin: 'com.android.application'
// Crashlytics integration is done as part of Firebase now, so it gets
// automagically activated with google-services.json
if (googleServicesEnabled) {
apply plugin: 'io.fabric'
apply plugin: 'com.google.firebase.crashlytics'
}
// Use the number of seconds/10 since Jan 1 2019 as the versionCode.
@@ -70,16 +70,11 @@ android {
}
}
repositories {
maven { url 'https://maven.fabric.io/public' }
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.appcompat:appcompat:1.2.0'
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.0-beta-5'
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.2'
if (!rootProject.ext.libreBuild) {
implementation 'com.google.android.gms:play-services-auth:16.0.1'
@@ -87,9 +82,9 @@ dependencies {
// Firebase
// - Crashlytics
// - Dynamic Links
implementation 'com.google.firebase:firebase-core:16.0.6'
implementation 'com.crashlytics.sdk.android:crashlytics:2.9.8'
implementation 'com.google.firebase:firebase-dynamic-links:16.1.5'
implementation 'com.google.firebase:firebase-analytics:17.5.0'
implementation 'com.google.firebase:firebase-crashlytics:17.2.1'
implementation 'com.google.firebase:firebase-dynamic-links:19.1.0'
}
implementation project(':sdk')

View File

@@ -85,8 +85,4 @@
# ^^^ We added the above when we switched minifyEnabled on.
# Rule to avoid build errors related to SVGs.
-keep public class com.horcrux.svg.** {*;}
# Hermes
-keep class com.facebook.hermes.unicode.** { *; }
-keep public class com.horcrux.svg.** {*;}

View File

@@ -3,9 +3,8 @@ package org.jitsi.meet;
import android.net.Uri;
import android.util.Log;
import com.crashlytics.android.Crashlytics;
import com.google.firebase.crashlytics.FirebaseCrashlytics;
import com.google.firebase.dynamiclinks.FirebaseDynamicLinks;
import io.fabric.sdk.android.Fabric;
import org.jitsi.meet.sdk.JitsiMeet;
import org.jitsi.meet.sdk.JitsiMeetActivity;
@@ -22,10 +21,7 @@ final class GoogleServicesHelper {
if (BuildConfig.GOOGLE_SERVICES_ENABLED) {
Log.d(activity.getClass().getSimpleName(), "Initializing Google Services");
if (!JitsiMeet.isCrashReportingDisabled(activity)) {
Fabric.with(activity, new Crashlytics());
}
FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(!JitsiMeet.isCrashReportingDisabled(activity));
FirebaseDynamicLinks.getInstance().getDynamicLink(activity.getIntent())
.addOnSuccessListener(activity, pendingDynamicLinkData -> {
Uri dynamicLink = null;

View File

@@ -7,26 +7,46 @@ buildscript {
repositories {
google()
jcenter()
repositories {
maven { url 'https://maven.fabric.io/public' }
}
}
dependencies {
classpath 'com.android.tools.build:gradle:3.3.2'
classpath 'com.android.tools.build:gradle:4.0.1'
classpath 'com.google.gms:google-services:4.3.3'
classpath 'io.fabric.tools:gradle:1.28.1'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files.
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.3.0'
}
}
ext {
buildToolsVersion = "29.0.3"
compileSdkVersion = 29
minSdkVersion = 23
targetSdkVersion = 29
supportLibVersion = "28.0.0"
// The Maven artifact groupdId of the third-party react-native modules which
// Jitsi Meet SDK for Android depends on and which are not available in
// third-party Maven repositories so we have to deploy to a Maven repository
// of ours.
moduleGroupId = 'com.facebook.react'
// Maven repo where artifacts will be published
mavenRepo = System.env.MVN_REPO ?: ""
mavenUser = System.env.MVN_USER ?: ""
mavenPassword = System.env.MVN_PASSWORD ?: ""
// Libre build
libreBuild = (System.env.LIBRE_BUILD ?: "false").toBoolean()
googleServicesEnabled = project.file('app/google-services.json').exists() && !libreBuild
}
allprojects {
repositories {
google()
jcenter()
// React Native (JS, Obj-C sources, Android binaries) is installed from npm.
maven { url "$rootDir/../node_modules/react-native/android" }
// Android JSC is installed from npm.
maven { url("$rootDir/../node_modules/jsc-android/dist") }
}
// Make sure we use the react-native version in node_modules and not the one
@@ -141,30 +161,6 @@ allprojects {
}
}
ext {
buildToolsVersion = "29.0.3"
compileSdkVersion = 29
minSdkVersion = 23
targetSdkVersion = 29
supportLibVersion = "28.0.0"
// The Maven artifact groupdId of the third-party react-native modules which
// Jitsi Meet SDK for Android depends on and which are not available in
// third-party Maven repositories so we have to deploy to a Maven repository
// of ours.
moduleGroupId = 'com.facebook.react'
// Maven repo where artifacts will be published
mavenRepo = System.env.MVN_REPO ?: ""
mavenUser = System.env.MVN_USER ?: ""
mavenPassword = System.env.MVN_PASSWORD ?: ""
// Libre build
libreBuild = (System.env.LIBRE_BUILD ?: "false").toBoolean()
googleServicesEnabled = project.file('app/google-services.json').exists() && !libreBuild
}
// Force the version of the Android build tools we have chosen on all
// subprojects. The forcing was introduced for react-native and the third-party
// modules that we utilize such as react-native-background-timer.

View File

@@ -10,7 +10,8 @@
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
# Default value: -Xmx10248m -XX:MaxPermSize=256m
# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
@@ -20,5 +21,5 @@
android.useAndroidX=true
android.enableJetifier=true
appVersion=20.4.0
sdkVersion=2.10.0
appVersion=20.5.0
sdkVersion=2.11.0

View File

@@ -1,6 +1,6 @@
#Fri Mar 08 13:36:51 CET 2019
#Wed Sep 23 11:48:00 EEST 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-all.zip

View File

@@ -10,7 +10,7 @@ MVN_HTTP=0
DEFAULT_SDK_VERSION=$(grep sdkVersion ${THIS_DIR}/../gradle.properties | cut -d"=" -f2)
SDK_VERSION=${OVERRIDE_SDK_VERSION:-${DEFAULT_SDK_VERSION}}
RN_VERSION=$(jq -r '.version' ${THIS_DIR}/../../node_modules/react-native/package.json)
HERMES_VERSION=$(jq -r '.dependencies."hermes-engine"' ${THIS_DIR}/../../node_modules/react-native/package.json | cut -c 2-)
JSC_VERSION="r"$(jq -r '.dependencies."jsc-android"' ${THIS_DIR}/../../node_modules/react-native/package.json | cut -d . -f 1 | cut -c 2-)
DO_GIT_TAG=${GIT_TAG:-0}
if [[ $THE_MVN_REPO == http* ]]; then
@@ -38,19 +38,17 @@ if [[ $MVN_HTTP == 1 ]]; then
-DgeneratePom=false \
-DpomFile=react-native-${RN_VERSION}.pom || true
popd
# Push Hermes
echo "Pushing Hermes ${HERMES_VERSION} to the Maven repo"
pushd ${THIS_DIR}/../../node_modules/hermes-engine/android/
# Push JSC
echo "Pushing JSC ${JSC_VERSION} to the Maven repo"
pushd ${THIS_DIR}/../../node_modules/jsc-android/dist/org/webkit/android-jsc/${JSC_VERSION}
mvn \
deploy:deploy-file \
-Durl=${MVN_REPO} \
-DrepositoryId=${MVN_REPO_ID} \
-Dfile=hermes-release.aar \
-Dfile=android-jsc-${JSC_VERSION}.aar \
-Dpackaging=aar \
-DgroupId=com.facebook \
-DartifactId=hermes \
-Dversion=${HERMES_VERSION} \
-DgeneratePom=true || true
-DgeneratePom=false \
-DpomFile=android-jsc-${JSC_VERSION}.pom || true
popd
else
# Push React Native, if necessary
@@ -67,19 +65,17 @@ else
popd
fi
# Push Hermes, if necessary
if [[ ! -d ${MVN_REPO}/com/facebook/hermes/${HERMES_VERSION} ]]; then
echo "Pushing Hermes ${HERMES_VERSION} to the Maven repo"
pushd ${THIS_DIR}/../../node_modules/hermes-engine/android/
# Push JSC, if necessary
if [[ ! -d ${MVN_REPO}/org/webkit/android-jsc/${JSC_VERSION} ]]; then
echo "Pushing JSC ${JSC_VERSION} to the Maven repo"
pushd ${THIS_DIR}/../../node_modules/jsc-android/dist/org/webkit/android-jsc/${JSC_VERSION}
mvn \
deploy:deploy-file \
-Durl=${MVN_REPO} \
-Dfile=hermes-release.aar \
-Dfile=android-jsc-${JSC_VERSION}.aar \
-Dpackaging=aar \
-DgroupId=com.facebook \
-DartifactId=hermes \
-Dversion=${HERMES_VERSION} \
-DgeneratePom=true
-DgeneratePom=false \
-DpomFile=android-jsc-${JSC_VERSION}.pom
popd
fi

View File

@@ -1,5 +1,3 @@
import groovy.json.JsonSlurper
apply plugin: 'com.android.library'
apply plugin: 'maven-publish'
@@ -35,26 +33,19 @@ android {
}
}
}
packagingOptions {
pickFirst '**/libc++_shared.so'
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.fragment:fragment:1.2.0'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.fragment:fragment:1.2.5'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
//noinspection GradleDynamicVersion
api 'com.facebook.react:react-native:+'
// Hermes JS engine
def hermesPath = "../../node_modules/hermes-engine/android/"
debugImplementation files(hermesPath + "hermes-debug.aar")
releaseImplementation files(hermesPath + "hermes-release.aar")
//noinspection GradleDynamicVersion
implementation 'org.webkit:android-jsc:+'
implementation 'com.dropbox.core:dropbox-core-sdk:3.0.8'
implementation 'com.jakewharton.timber:timber:4.7.1'
@@ -153,7 +144,7 @@ android.libraryVariants.all { def variant ->
mergeResourcesTask.dependsOn(currentBundleTask)
mergeAssetsTask.doLast {
def assetsDir = mergeAssetsTask.outputDir
def assetsDir = mergeAssetsTask.outputDir.get()
// Bundle sounds
//
@@ -187,7 +178,7 @@ android.libraryVariants.all { def variant ->
if (currentBundleTask.enabled) {
copy {
from(resourcesDir)
into(mergeResourcesTask.outputDir)
into(mergeResourcesTask.outputDir.get())
}
}
}
@@ -227,14 +218,6 @@ publishing {
dependency.appendNode('artifactId', artifactId)
dependency.appendNode('version', it.moduleVersion)
}
// Add Hermes dependency.
def hermesPkg = new File("$rootDir/../node_modules/hermes-engine/package.json")
def hermesVersion = new JsonSlurper().parseText(hermesPkg.text).version
def hermesDependency = dependencies.appendNode('dependency')
hermesDependency.appendNode('groupId', "com.facebook")
hermesDependency.appendNode('artifactId', "hermes")
hermesDependency.appendNode('version', hermesVersion)
}
}

View File

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

View File

@@ -20,7 +20,6 @@ import android.app.Activity;
import androidx.annotation.Nullable;
import com.facebook.hermes.reactexecutor.HermesExecutorFactory;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
@@ -28,6 +27,7 @@ import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.common.LifecycleState;
import com.facebook.react.devsupport.DevInternalSettings;
import com.facebook.react.jscexecutor.JSCExecutorFactory;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.facebook.react.uimanager.ViewManager;
import com.facebook.soloader.SoLoader;
@@ -35,6 +35,7 @@ import com.facebook.soloader.SoLoader;
import com.oney.WebRTCModule.RTCVideoViewManager;
import com.oney.WebRTCModule.WebRTCModule;
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
import org.webrtc.SoftwareVideoDecoderFactory;
import org.webrtc.SoftwareVideoEncoderFactory;
import org.webrtc.audio.AudioDeviceModule;
@@ -216,8 +217,9 @@ class ReactInstanceManagerHolder {
// Ignore any error, the module is not compiled when LIBRE_BUILD is enabled.
}
// Use the Hermes JavaScript engine.
HermesExecutorFactory jsFactory = new HermesExecutorFactory();
// Keep on using JSC, the jury is out on Hermes.
JSCExecutorFactory jsFactory
= new JSCExecutorFactory("", "");
reactInstanceManager
= ReactInstanceManager.builder()

7
app.js
View File

@@ -4,6 +4,8 @@ import 'jquery';
import 'jquery-contextmenu';
import 'jQuery-Impromptu';
import 'olm';
import conference from './conference';
import API from './modules/API';
import UI from './modules/UI/UI';
@@ -11,6 +13,11 @@ import keyboardshortcut from './modules/keyboardshortcut/keyboardshortcut';
import remoteControl from './modules/remotecontrol/RemoteControl';
import translation from './modules/translation/translation';
// Initialize Olm as early as possible.
if (window.Olm) {
window.Olm.init();
}
window.APP = {
API,
conference,

View File

@@ -110,7 +110,6 @@ import {
} from './react/features/base/util';
import { showDesktopPicker } from './react/features/desktop-picker';
import { appendSuffix } from './react/features/display-name';
import { setE2EEKey } from './react/features/e2ee';
import {
maybeOpenFeedbackDialog,
submitFeedback
@@ -121,7 +120,8 @@ import { suspendDetected } from './react/features/power-monitor';
import {
initPrejoin,
isPrejoinPageEnabled,
isPrejoinPageVisible
isPrejoinPageVisible,
makePrecallTest
} from './react/features/prejoin';
import { createRnnoiseProcessorPromise } from './react/features/rnnoise';
import { toggleScreenshotCaptureEffect } from './react/features/screenshot-capture';
@@ -745,8 +745,6 @@ export default {
this.roomName = roomName;
window.addEventListener('hashchange', this.onHashChange.bind(this), false);
try {
// Initialize the device list first. This way, when creating tracks
// based on preferred devices, loose label matching can be done in
@@ -759,7 +757,15 @@ export default {
}
if (isPrejoinPageEnabled(APP.store.getState())) {
_connectionPromise = connect(roomName);
_connectionPromise = connect(roomName).then(c => {
// we want to initialize it early, in case of errors to be able
// to gather logs
APP.connection = c;
return c;
});
APP.store.dispatch(makePrecallTest(this._getConferenceOptions()));
const { tryCreateLocalTracks, errors } = this.createInitialLocalTracks(initialOptions);
const tracks = await tryCreateLocalTracks;
@@ -1206,10 +1212,6 @@ export default {
// end used by torture
getLogs() {
return room.getLogs();
},
/**
* Download logs, a function that can be called from console while
* debugging.
@@ -1218,7 +1220,7 @@ export default {
saveLogs(filename = 'meetlog.json') {
// this can be called from console and will not have reference to this
// that's why we reference the global var
const logs = APP.conference.getLogs();
const logs = APP.connection.getLogs();
const data = encodeURIComponent(JSON.stringify(logs, null, ' '));
const elem = document.createElement('a');
@@ -1234,34 +1236,6 @@ export default {
}));
},
/**
* Handled location hash change events.
*/
onHashChange() {
const items = {};
const parts = window.location.hash.substr(1).split('&');
for (const part of parts) {
const param = part.split('=');
const key = param[0];
if (!key) {
continue; // eslint-disable-line no-continue
}
items[key] = param[1];
}
if (typeof items.e2eekey !== 'undefined') {
APP.store.dispatch(setE2EEKey(items.e2eekey));
// Clean URL in browser history.
const cleanUrl = window.location.href.split('#')[0];
history.replaceState(history.state, document.title, cleanUrl);
}
},
/**
* Exposes a Command(s) API on this instance. It is necessitated by (1) the
* desire to keep room private to this instance and (2) the need of other
@@ -2856,7 +2830,14 @@ export default {
this._room = undefined;
room = undefined;
APP.API.notifyReadyToClose();
/**
* Don't call {@code notifyReadyToClose} if the promotional page flag is set
* and let the page take care of sending the message, since there will be
* a redirect to the page regardlessly.
*/
if (!interfaceConfig.SHOW_PROMOTIONAL_CLOSE_PAGE) {
APP.API.notifyReadyToClose();
}
APP.store.dispatch(maybeRedirectToWelcomePage(values[0]));
});
},

View File

@@ -14,9 +14,6 @@ var config = {
// Domain for authenticated users. Defaults to <domain>.
// authdomain: 'jitsi-meet.example.com',
// Jirecon recording component domain.
// jirecon: 'jirecon.jitsi-meet.example.com',
// Call control component (Jigasi).
// call_control: 'callcontrol.jitsi-meet.example.com',
@@ -67,6 +64,11 @@ var config = {
// adjusted to 2.5 Mbps. This takes a value between 0 and 1 which determines
// the probability for this to be enabled.
// capScreenshareBitrate: 1 // 0 to disable
// Enable callstats only for a percentage of users.
// This takes a value between 0 and 100 which determines the probability for
// the callstats to be enabled.
// callStatsThreshold: 5 // enable callstats for 5% of the users.
},
// Disables ICE/UDP by filtering out local and remote UDP candidates in
@@ -118,6 +120,9 @@ var config = {
// Valid values are in the range 6000 to 510000
// opusMaxAverageBitrate: 20000,
// Enables redundancy for Opus
// enableOpusRed: false
// Video
// Sets the preferred resolution (height) for local video. Defaults to 720.
@@ -125,7 +130,7 @@ var config = {
// How many participants while in the tile view mode, before the receiving video quality is reduced from HD to SD.
// Use -1 to disable.
// maxFullResolutionParticipants: 2
// maxFullResolutionParticipants: 2,
// w3c spec-compliant video constraints to use for video capture. Currently
// used by browsers that return true from lib-jitsi-meet's
@@ -161,6 +166,7 @@ var config = {
// Note that it's not recommended to do this because simulcast is not
// supported when using H.264. For 1-to-1 calls this setting is enabled by
// default and can be toggled in the p2p section.
// This option has been deprecated, use preferredCodec under videoQuality section instead.
// preferH264: true,
// If set to true, disable H.264 video codec by stripping it out of the
@@ -234,6 +240,18 @@ var config = {
// Specify the settings for video quality optimizations on the client.
// videoQuality: {
// // Provides a way to prevent a video codec from being negotiated on the JVB connection. The codec specified
// // here will be removed from the list of codecs present in the SDP answer generated by the client. If the
// // same codec is specified for both the disabled and preferred option, the disable settings will prevail.
// // Note that 'VP8' cannot be disabled since it's a mandatory codec, the setting will be ignored in this case.
// disabledCodec: 'H264',
//
// // Provides a way to set a preferred video codec for the JVB connection. If 'H264' is specified here,
// // simulcast will be automatically disabled since JVB doesn't support H264 simulcast yet. This will only
// // rearrange the the preference order of the codecs in the SDP answer generated by the browser only if the
// // preferred codec specified here is present. Please ensure that the JVB offers the specified codec for this
// // to take effect.
// preferredCodec: 'VP8',
//
// // Provides a way to configure the maximum bitrates that will be enforced on the simulcast streams for
// // video tracks. The keys in the object represent the type of the stream (LD, SD or HD) and the values
@@ -244,6 +262,21 @@ var config = {
// low: 200000,
// standard: 500000,
// high: 1500000
// },
//
// // The options can be used to override default thresholds of video thumbnail heights corresponding to
// // the video quality levels used in the application. At the time of this writing the allowed levels are:
// // 'low' - for the low quality level (180p at the time of this writing)
// // 'standard' - for the medium quality level (360p)
// // 'high' - for the high quality level (720p)
// // The keys should be positive numbers which represent the minimal thumbnail height for the quality level.
// //
// // With the default config value below the application will use 'low' quality until the thumbnails are
// // at least 360 pixels tall. If the thumbnail height reaches 720 pixels then the application will switch to
// // the high quality.
// minHeightForQualityLvl: {
// 360: 'standard,
// 720: 'high'
// }
// },
@@ -304,11 +337,15 @@ var config = {
// 'datachannel'), undefined (treat it as 'datachannel') and false (don't
// open any channel).
// openBridgeChannel: true,
openBridgeChannel: 'websocket',
// UI
//
// Hides lobby button
// hideLobbyButton: false,
// Require users to always specify a display name.
// requireDisplayName: true,
@@ -357,6 +394,10 @@ var config = {
// set or the lobby is not enabled.
// enableInsecureRoomNameWarning: false,
// Whether to automatically copy invitation URL after creating a room.
// Document should be focused for this option to work
// enableAutomaticUrlCopy: false,
// Stats
//
@@ -420,13 +461,20 @@ var config = {
// iceTransportPolicy: 'all',
// If set to true, it will prefer to use H.264 for P2P calls (if H.264
// is supported).
// is supported). This setting is deprecated, use preferredCodec instead.
// preferH264: true
// Provides a way to set the video codec preference on the p2p connection. Acceptable
// codec values are 'VP8', 'VP9' and 'H264'.
// preferredCodec: 'H264',
// If set to true, disable H.264 video codec by stripping it out of the
// SDP.
// SDP. This setting is deprecated, use disabledCodec instead.
// disableH264: false,
// Provides a way to prevent a video codec from being negotiated on the p2p connection.
// disabledCodec: '',
// How long we're going to wait, before going back to P2P after the 3rd
// participant has left the conference (to filter out page reload).
// backToP2PDelay: 5
@@ -444,6 +492,12 @@ var config = {
// amplitudeAPPKey: '<APP_KEY>'
// Configuration for the rtcstats server:
// By enabling rtcstats server every time a conference is joined the rtcstats
// module connects to the provided rtcstatsEndpoint and sends statistics regarding
// PeerConnection states along with getStats metrics polled at the specified
// interval.
// rtcstatsEnabled: true,
// In order to enable rtcstats one needs to provide a endpoint url.
// rtcstatsEndpoint: wss://rtcstats-server-pilot.jitsi.net/,
@@ -459,6 +513,9 @@ var config = {
// ],
},
// Logs that should go be passed through the 'log' event if a handler is defined for it
// apiLogLevels: ['warn', 'log', 'error', 'info', 'debug'],
// Information about the jitsi-meet instance we are connecting to, including
// the user region as seen by the server.
deploymentInfo: {
@@ -581,8 +638,6 @@ var config = {
// List of undocumented settings used in jitsi-meet
/**
_immediateReloadThreshold
autoRecord
autoRecordToken
debug
debugAudioLevels
deploymentInfo
@@ -605,6 +660,13 @@ var config = {
tokenAuthUrl
*/
/**
* This property can be used to alter the generated meeting invite links (in combination with a branding domain
* which is retrieved internally by jitsi meet) (e.g. https://meet.jit.si/someMeeting
* can become https://brandedDomain/roomAlias)
*/
// brandingRoomAlias: null,
// List of undocumented settings used in lib-jitsi-meet
/**
_peerConnStatusOutOfLastNTimeout

View File

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

View File

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

View File

@@ -1,7 +1,6 @@
#e2ee-section {
.title {
font-weight: 700;
}
display: flex;
flex-direction: column;
.description {
font-size: 13px;
@@ -13,29 +12,15 @@
}
}
.key-field {
align-items: center;
.control-row {
display: flex;
flex-direction: row;
justify-content: space-between;
margin-top: 15px;
label {
font-size: 14px;
font-weight: 700;
}
input {
background-color: inherit;
border: none;
color: inherit;
flex: 1;
padding: 0 5px;
}
a {
color: #6FB1EA;
cursor: pointer;
font-size: 14px;
text-decoration: none;
font-weight: bold;
}
}
}

View File

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

View File

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

View File

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

2
debian/control vendored
View File

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

View File

@@ -45,8 +45,12 @@ case "$1" in
rm -rf /var/lib/prosody/$JICOFO_AUTH_DOMAIN.*
rm -rf /var/lib/prosody/$JVB_HOSTNAME.*
# clean created users
rm -rf /var/lib/prosody/`echo $JICOFO_AUTH_DOMAIN | sed -e "s/\./%2e/g"`
# clean created users, replace '.' with '%2e', replace '-' with '%2d'
rm -rf /var/lib/prosody/`echo $JICOFO_AUTH_DOMAIN | sed -e "s/\./%2e/g"| sed -e "s/-/%2d/g"`
# clean the prosody cert from the trust store
rm -rf /usr/local/share/ca-certificates/$JICOFO_AUTH_DOMAIN.*
update-ca-certificates -f
fi
# Clear the debconf variable

View File

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

View File

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

View File

@@ -36,26 +36,6 @@ case "$1" in
NGINX_CONFIG="/etc/nginx/sites-available/$JVB_HOSTNAME.conf"
JITSI_MEET_CONFIG="/etc/jitsi/meet/$JVB_HOSTNAME-config.js"
NGINX_SITES_ENABLED="/etc/nginx/sites-enabled/"
NGINX_CONFIG_ENABLED="${NGINX_SITES_ENABLED}${JVB_HOSTNAME}.conf"
NGINX_MULTIPLEXING="true"
for site in ${NGINX_SITES_ENABLED}*; do
# if it is not a file continue
[ -f "${site}" ] || continue
# if it is our config skip
[ "${site}" != "${NGINX_CONFIG_ENABLED}" ] || continue
# check whether other enabled hosts has listen 443
if cat ${site} | grep -v "^[[:space:]]*#" | grep listen | grep -q "^.*[[:space:]:]443[;[:space:]].*" ; then
# nothing to do
echo "------------------------------------------------"
echo ""
echo "turnserver is listening on tcp 5349 as other nginx sites use port 443"
echo ""
echo "------------------------------------------------"
NGINX_MULTIPLEXING="false"
fi
done
# if there was a turn config backup it so we can configure
# we cannot recognize at the moment is this a user config or default config when installing coturn
if [[ -f $TURN_CONFIG ]] && ! grep -q "jitsi-meet coturn config" "$TURN_CONFIG" ; then
@@ -133,19 +113,9 @@ denied-peer-ip=240.0.0.0-255.255.255.255" >> $TURN_CONFIG
TURN_SECRET="$RET"
# no turn config exists, lt's copy template and fill it in
PUBLIC_IP=$(dig -4 +short myip.opendns.com a @resolver1.opendns.com) || true
if [ -z "$PUBLIC_IP" ] ; then
PUBLIC_IP="127.0.0.1"
echo "------------------------------------------------"
echo "Warning! Could not resolve your external ip address! Error:^"
echo "Your turn server will not work till you edit your $TURN_CONFIG config file."
echo "You need to set your external ip address in external-ip and restart coturn service."
echo "------------------------------------------------"
fi
cp /usr/share/jitsi-meet-turnserver/turnserver.conf $TURN_CONFIG
sed -i "s/jitsi-meet.example.com/$JVB_HOSTNAME/g" $TURN_CONFIG
sed -i "s/__turnSecret__/$TURN_SECRET/g" $TURN_CONFIG
sed -i "s/__external_ip_address__/$PUBLIC_IP/g" $TURN_CONFIG
# SSL for nginx
db_get jitsi-meet/cert-choice
@@ -170,18 +140,14 @@ denied-peer-ip=240.0.0.0-255.255.255.255" >> $TURN_CONFIG
invoke-rc.d coturn restart || true
NGINX_STREAM_CONFIG="/etc/nginx/modules-enabled/60-jitsi-meet.conf"
if [ $NGINX_MULTIPLEXING = "true" ] && [ ! -f $NGINX_STREAM_CONFIG ] && [ -f $NGINX_CONFIG ] ; then
ln -s /usr/share/jitsi-meet-turnserver/jitsi-meet.conf $NGINX_STREAM_CONFIG
sed -i "s/listen 443 ssl/listen 4444 ssl http2/g" $NGINX_CONFIG
sed -i "s/listen \[\:\:\]\:443 ssl/listen \[\:\:\]\:4444 ssl http2/g" $NGINX_CONFIG
invoke-rc.d nginx reload || true
else
PROSODY_HOST_CONFIG="/etc/prosody/conf.avail/$JVB_HOSTNAME.cfg.lua"
if [ -f $PROSODY_HOST_CONFIG ] ; then
# If we are not multiplexing we need to change the port in prosody config
sed -i 's/"443"/"5349"/g' $PROSODY_HOST_CONFIG
invoke-rc.d prosody restart || true
fi
if [ -f $NGINX_STREAM_CONFIG ] ; then
echo "------------------------------------------------"
echo ""
echo "You have multiplexing enabled, it is recommended to disable it and migrate to using websockets for the bridge channel."
echo "The support for sctp data channels is deprecated and will be dropped at some point."
echo "How to do it at: https://jitsi.org/multiplexing-to-bridge-ws-howto"
echo ""
echo "------------------------------------------------"
fi
# Enable turn server in config.js

View File

@@ -24,7 +24,6 @@ set -e
case "$1" in
remove)
rm -rf /etc/nginx/modules-enabled/60-jitsi-meet.conf
if [ -x "/etc/init.d/nginx" ]; then
invoke-rc.d nginx reload || true
fi
@@ -33,7 +32,6 @@ case "$1" in
fi
;;
purge)
rm -rf /etc/nginx/modules-enabled/60-jitsi-meet.conf
rm -rf /etc/turnserver.conf
if [ -x "/etc/init.d/nginx" ]; then
invoke-rc.d nginx reload || true

View File

@@ -8,7 +8,7 @@ turncredentials_secret = "__turnSecret__";
turncredentials = {
{ type = "stun", host = "jitmeet.example.com", port = "3478" },
{ type = "turn", host = "jitmeet.example.com", port = "3478", transport = "udp" },
{ type = "turns", host = "jitmeet.example.com", port = "443", transport = "tcp" }
{ type = "turns", host = "jitmeet.example.com", port = "5349", transport = "tcp" }
};
cross_domain_bosh = false;

View File

@@ -12,7 +12,6 @@ no-tcp-relay
no-tcp
listening-port=3478
tls-listening-port=5349
external-ip=__external_ip_address__
no-tlsv1
no-tlsv1_1
# https://ssl-config.mozilla.org/#server=haproxy&version=2.1&config=intermediate&openssl=1.1.0g&guideline=5.4

View File

@@ -1,7 +1,11 @@
# this is jitsi-meet nginx module configuration
# this forward all http traffic to the nginx virtual host port
# and the rest to the turn server
#
# Multiplexing based on ALPN is DEPRECATED. ALPN does not play well with websockets on some browsers and reverse proxies.
# To migrate away from using it read: https://jitsi.org/multiplexing-to-bridge-ws-howto
# This file will be removed at some point and if deployment is still using it, will break.
#
stream {
upstream web {
server 127.0.0.1:4444;

View File

@@ -45,8 +45,10 @@ server {
error_page 404 /static/404.html;
gzip on;
gzip_types text/plain text/css application/javascript application/json;
gzip_types text/plain text/css application/javascript application/json image/x-icon application/octet-stream application/wasm;
gzip_vary on;
gzip_proxied no-cache no-store private expired auth;
gzip_min_length 512;
location = /config.js {
alias /etc/jitsi/meet/jitsi-meet.example.com-config.js;
@@ -61,6 +63,11 @@ server {
{
add_header 'Access-Control-Allow-Origin' '*';
alias /usr/share/jitsi-meet/$1/$2;
# cache all versioned files
if ($arg_v) {
expires 1y;
}
}
# BOSH
@@ -80,6 +87,15 @@ server {
tcp_nodelay on;
}
# colibri (JVB) websockets for jvb1
location ~ ^/colibri-ws/default-id/(.*) {
proxy_pass http://127.0.0.1:9090/colibri-ws/default-id/$1$is_args$args;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
tcp_nodelay on;
}
location ~ ^/([^/?&:'"]+)$ {
try_files $uri @root_path;
}

View File

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

View File

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

View File

@@ -5,10 +5,9 @@ require_relative '../node_modules/@react-native-community/cli-platform-ios/nativ
target 'jitsi-meet' do
project 'app/app.xcodeproj'
pod 'Crashlytics', '~> 3.14.0'
pod 'Fabric', '~> 1.10.2'
pod 'Firebase/Core', '~> 6.16.0'
pod 'Firebase/DynamicLinks', '~> 6.16.0'
pod 'Firebase/Analytics', '~> 6.33.0'
pod 'Firebase/Crashlytics', '~> 6.33.0'
pod 'Firebase/DynamicLinks', '~> 6.33.0'
end
target 'JitsiMeet' do

View File

@@ -11,10 +11,7 @@ PODS:
- CocoaLumberjack (3.5.3):
- CocoaLumberjack/Core (= 3.5.3)
- CocoaLumberjack/Core (3.5.3)
- Crashlytics (3.14.0):
- Fabric (~> 1.10.2)
- DoubleConversion (1.1.6)
- Fabric (1.10.2)
- FBLazyVector (0.61.5-jitsi.1)
- FBReactNativeSpec (0.61.5-jitsi.1):
- Folly (= 2018.10.22.00)
@@ -23,48 +20,50 @@ PODS:
- React-Core (= 0.61.5-jitsi.1)
- React-jsi (= 0.61.5-jitsi.1)
- ReactCommon/turbomodule/core (= 0.61.5-jitsi.1)
- Firebase/Core (6.16.0):
- Firebase/Analytics (6.33.0):
- Firebase/Core
- Firebase/Core (6.33.0):
- Firebase/CoreOnly
- FirebaseAnalytics (= 6.2.2)
- Firebase/CoreOnly (6.16.0):
- FirebaseCore (= 6.6.1)
- Firebase/DynamicLinks (6.16.0):
- FirebaseAnalytics (= 6.8.3)
- Firebase/CoreOnly (6.33.0):
- FirebaseCore (= 6.10.3)
- Firebase/Crashlytics (6.33.0):
- Firebase/CoreOnly
- FirebaseDynamicLinks (~> 4.0.6)
- FirebaseAnalytics (6.2.2):
- FirebaseCore (~> 6.6)
- FirebaseInstanceID (~> 4.3)
- GoogleAppMeasurement (= 6.2.2)
- GoogleUtilities/AppDelegateSwizzler (~> 6.0)
- GoogleUtilities/MethodSwizzler (~> 6.0)
- GoogleUtilities/Network (~> 6.0)
- "GoogleUtilities/NSData+zlib (~> 6.0)"
- nanopb (= 0.3.9011)
- FirebaseAnalyticsInterop (1.5.0)
- FirebaseCore (6.6.1):
- FirebaseCoreDiagnostics (~> 1.2)
- FirebaseCoreDiagnosticsInterop (~> 1.2)
- GoogleUtilities/Environment (~> 6.5)
- GoogleUtilities/Logger (~> 6.5)
- FirebaseCoreDiagnostics (1.2.2):
- FirebaseCoreDiagnosticsInterop (~> 1.2)
- GoogleDataTransportCCTSupport (~> 2.0)
- GoogleUtilities/Environment (~> 6.5)
- GoogleUtilities/Logger (~> 6.5)
- nanopb (~> 0.3.901)
- FirebaseCoreDiagnosticsInterop (1.2.0)
- FirebaseDynamicLinks (4.0.8):
- FirebaseAnalyticsInterop (~> 1.3)
- FirebaseCore (~> 6.2)
- FirebaseInstallations (1.1.1):
- FirebaseCore (~> 6.6)
- GoogleUtilities/UserDefaults (~> 6.5)
- FirebaseCrashlytics (~> 4.6.1)
- Firebase/DynamicLinks (6.33.0):
- Firebase/CoreOnly
- FirebaseDynamicLinks (~> 4.3.1)
- FirebaseAnalytics (6.8.3):
- FirebaseCore (~> 6.10)
- FirebaseInstallations (~> 1.6)
- GoogleAppMeasurement (= 6.8.3)
- GoogleUtilities/AppDelegateSwizzler (~> 6.7)
- GoogleUtilities/MethodSwizzler (~> 6.7)
- GoogleUtilities/Network (~> 6.7)
- "GoogleUtilities/NSData+zlib (~> 6.7)"
- nanopb (~> 1.30906.0)
- FirebaseCore (6.10.3):
- FirebaseCoreDiagnostics (~> 1.6)
- GoogleUtilities/Environment (~> 6.7)
- GoogleUtilities/Logger (~> 6.7)
- FirebaseCoreDiagnostics (1.7.0):
- GoogleDataTransport (~> 7.4)
- GoogleUtilities/Environment (~> 6.7)
- GoogleUtilities/Logger (~> 6.7)
- nanopb (~> 1.30906.0)
- FirebaseCrashlytics (4.6.1):
- FirebaseCore (~> 6.10)
- FirebaseInstallations (~> 1.6)
- GoogleDataTransport (~> 7.2)
- nanopb (~> 1.30906.0)
- PromisesObjC (~> 1.2)
- FirebaseDynamicLinks (4.3.1):
- FirebaseCore (~> 6.10)
- FirebaseInstallations (1.7.0):
- FirebaseCore (~> 6.10)
- GoogleUtilities/Environment (~> 6.7)
- GoogleUtilities/UserDefaults (~> 6.7)
- PromisesObjC (~> 1.2)
- FirebaseInstanceID (4.3.2):
- FirebaseCore (~> 6.6)
- FirebaseInstallations (~> 1.0)
- GoogleUtilities/Environment (~> 6.5)
- GoogleUtilities/UserDefaults (~> 6.5)
- Folly (2018.10.22.00):
- boost-for-react-native
- DoubleConversion
@@ -75,37 +74,36 @@ PODS:
- DoubleConversion
- glog
- glog (0.3.5)
- GoogleAppMeasurement (6.2.2):
- GoogleUtilities/AppDelegateSwizzler (~> 6.0)
- GoogleUtilities/MethodSwizzler (~> 6.0)
- GoogleUtilities/Network (~> 6.0)
- "GoogleUtilities/NSData+zlib (~> 6.0)"
- nanopb (= 0.3.9011)
- GoogleDataTransport (5.1.0)
- GoogleDataTransportCCTSupport (2.0.1):
- GoogleDataTransport (~> 5.1)
- nanopb (~> 0.3.901)
- GoogleAppMeasurement (6.8.3):
- GoogleUtilities/AppDelegateSwizzler (~> 6.7)
- GoogleUtilities/MethodSwizzler (~> 6.7)
- GoogleUtilities/Network (~> 6.7)
- "GoogleUtilities/NSData+zlib (~> 6.7)"
- nanopb (~> 1.30906.0)
- GoogleDataTransport (7.4.0):
- nanopb (~> 1.30906.0)
- GoogleSignIn (5.0.1):
- AppAuth (~> 1.2)
- GTMAppAuth (~> 1.0)
- GTMSessionFetcher/Core (~> 1.1)
- GoogleUtilities/AppDelegateSwizzler (6.5.2):
- GoogleUtilities/AppDelegateSwizzler (6.7.2):
- GoogleUtilities/Environment
- GoogleUtilities/Logger
- GoogleUtilities/Network
- GoogleUtilities/Environment (6.5.2)
- GoogleUtilities/Logger (6.5.2):
- GoogleUtilities/Environment (6.7.2):
- PromisesObjC (~> 1.2)
- GoogleUtilities/Logger (6.7.2):
- GoogleUtilities/Environment
- GoogleUtilities/MethodSwizzler (6.5.2):
- GoogleUtilities/MethodSwizzler (6.7.2):
- GoogleUtilities/Logger
- GoogleUtilities/Network (6.5.2):
- GoogleUtilities/Network (6.7.2):
- GoogleUtilities/Logger
- "GoogleUtilities/NSData+zlib"
- GoogleUtilities/Reachability
- "GoogleUtilities/NSData+zlib (6.5.2)"
- GoogleUtilities/Reachability (6.5.2):
- "GoogleUtilities/NSData+zlib (6.7.2)"
- GoogleUtilities/Reachability (6.7.2):
- GoogleUtilities/Logger
- GoogleUtilities/UserDefaults (6.5.2):
- GoogleUtilities/UserDefaults (6.7.2):
- GoogleUtilities/Logger
- GTMAppAuth (1.0.0):
- AppAuth/Core (~> 1.0)
@@ -115,13 +113,13 @@ PODS:
- GTMSessionFetcher/Core (1.2.2)
- GTMSessionFetcher/Full (1.2.2):
- GTMSessionFetcher/Core (= 1.2.2)
- nanopb (0.3.9011):
- nanopb/decode (= 0.3.9011)
- nanopb/encode (= 0.3.9011)
- nanopb/decode (0.3.9011)
- nanopb/encode (0.3.9011)
- nanopb (1.30906.0):
- nanopb/decode (= 1.30906.0)
- nanopb/encode (= 1.30906.0)
- nanopb/decode (1.30906.0)
- nanopb/encode (1.30906.0)
- ObjectiveDropboxOfficial (3.9.4)
- PromisesObjC (1.2.8)
- PromisesObjC (1.2.10)
- RCTRequired (0.61.5-jitsi.1)
- RCTTypeSafety (0.61.5-jitsi.1):
- FBLazyVector (= 0.61.5-jitsi.1)
@@ -285,7 +283,7 @@ PODS:
- React-cxxreact (= 0.61.5-jitsi.1)
- React-jsi (= 0.61.5-jitsi.1)
- React-jsinspector (0.61.5-jitsi.1)
- react-native-background-timer (2.1.1):
- react-native-background-timer (2.4.0):
- React
- react-native-calendar-events (2.0.0):
- React
@@ -295,7 +293,7 @@ PODS:
- React
- react-native-webrtc (1.84.0):
- React
- react-native-webview (7.4.1):
- react-native-webview (10.9.0):
- React
- React-RCTActionSheet (0.61.5-jitsi.1):
- React-Core/RCTActionSheetHeaders (= 0.61.5-jitsi.1)
@@ -373,13 +371,12 @@ DEPENDENCIES:
- Amplitude-iOS (~> 4.0.4)
- BVLinearGradient (from `../node_modules/react-native-linear-gradient`)
- CocoaLumberjack (~> 3.5.3)
- Crashlytics (~> 3.14.0)
- DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`)
- Fabric (~> 1.10.2)
- FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector/`)
- FBReactNativeSpec (from `../node_modules/react-native/Libraries/FBReactNativeSpec/`)
- Firebase/Core (~> 6.16.0)
- Firebase/DynamicLinks (~> 6.16.0)
- Firebase/Analytics (~> 6.33.0)
- Firebase/Crashlytics (~> 6.33.0)
- Firebase/DynamicLinks (~> 6.33.0)
- Folly (from `../node_modules/react-native/third-party-podspecs/Folly.podspec`)
- glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`)
- ObjectiveDropboxOfficial (~> 3.9.4)
@@ -424,20 +421,15 @@ SPEC REPOS:
- AppAuth
- boost-for-react-native
- CocoaLumberjack
- Crashlytics
- Fabric
- Firebase
- FirebaseAnalytics
- FirebaseAnalyticsInterop
- FirebaseCore
- FirebaseCoreDiagnostics
- FirebaseCoreDiagnosticsInterop
- FirebaseCrashlytics
- FirebaseDynamicLinks
- FirebaseInstallations
- FirebaseInstanceID
- GoogleAppMeasurement
- GoogleDataTransport
- GoogleDataTransportCCTSupport
- GoogleSignIn
- GoogleUtilities
- GTMAppAuth
@@ -530,32 +522,27 @@ SPEC CHECKSUMS:
boost-for-react-native: 39c7adb57c4e60d6c5479dd8623128eb5b3f0f2c
BVLinearGradient: e3aad03778a456d77928f594a649e96995f1c872
CocoaLumberjack: 2f44e60eb91c176d471fdba43b9e3eae6a721947
Crashlytics: 540b7e5f5da5a042647227a5e3ac51d85eed06df
DoubleConversion: 5805e889d232975c086db112ece9ed034df7a0b2
Fabric: 706c8b8098fff96c33c0db69cbf81f9c551d0d74
FBLazyVector: 4a5251159a3ed05dc11cc8b74cf937869935814b
FBReactNativeSpec: 6fa602a20993212cc9877a81838578ffb0008bc9
Firebase: 497158b816d0a86fc31babbd05546fcd7e6083ff
FirebaseAnalytics: cf95d3aab897612783020fbd98401d5366f135ee
FirebaseAnalyticsInterop: 3f86269c38ae41f47afeb43ebf32a001f58fcdae
FirebaseCore: 85064903ed6c28e47fec9c7bd149d94ba1b6b6e7
FirebaseCoreDiagnostics: e9b4cd8ba60dee0f2d13347332e4b7898cca5b61
FirebaseCoreDiagnosticsInterop: 296e2c5f5314500a850ad0b83e9e7c10b011a850
FirebaseDynamicLinks: 417dc6dbb6013233c77558290d73296f429656a6
FirebaseInstallations: acb3216eb9784d3b1d2d2d635ff74fa892cc0c44
FirebaseInstanceID: 7ee0d6777013bb952f377b41965bf132b6a075be
Firebase: 8db6f2d1b2c5e2984efba4949a145875a8f65fe5
FirebaseAnalytics: 5dd088bd2e67bb9d13dbf792d1164ceaf3052193
FirebaseCore: d889d9e12535b7f36ac8bfbf1713a0836a3012cd
FirebaseCoreDiagnostics: 770ac5958e1372ce67959ae4b4f31d8e127c3ac1
FirebaseCrashlytics: 5777d3462fb8c3ab9e80a2473bd7d667a2e8411c
FirebaseDynamicLinks: 6eac37d86910382eafb6315d952cc44c9e176094
FirebaseInstallations: 466c7b4d1f58fe16707693091da253726a731ed2
Folly: 30e7936e1c45c08d884aa59369ed951a8e68cf51
glog: 1f3da668190260b06b429bb211bfbee5cd790c28
GoogleAppMeasurement: d0560d915abf15e692e8538ba1d58442217b6aff
GoogleDataTransport: b29a21d813e906014ca16c00897827e40e4a24ab
GoogleDataTransportCCTSupport: 6f15a89b0ca35d6fa523e1f752ef818588885988
GoogleAppMeasurement: 966e88df9d19c15715137bb2ddaf52373f111436
GoogleDataTransport: b7f406340a291370045a270c599e53c6fa6ec20f
GoogleSignIn: 3a51b9bb8e48b635fd7f4272cee06ca260345b86
GoogleUtilities: ad0f3b691c67909d03a3327cc205222ab8f42e0e
GoogleUtilities: 7f2f5a07f888cdb145101d6042bc4422f57e70b3
GTMAppAuth: 4deac854479704f348309e7b66189e604cf5e01e
GTMSessionFetcher: 61bb0f61a4cb560030f1222021178008a5727a23
nanopb: 18003b5e52dab79db540fe93fe9579f399bd1ccd
nanopb: 59317e09cf1f1a0af72f12af412d54edf52603fc
ObjectiveDropboxOfficial: a5afefc83f6467c42c45f2253f583f2ad1ffc701
PromisesObjC: c119f3cd559f50b7ae681fa59dc1acd19173b7e6
PromisesObjC: b14b1c6b68e306650688599de8a45e49fae81151
RCTRequired: f63dd90a89a60602acdd44c42e5d2645ca60ab79
RCTTypeSafety: 24a3c6d55684046ed550b1d0ef083a9bf71c8bd4
React: 71c5a51135f291c3b32c0b558e167b858ae50e84
@@ -565,12 +552,12 @@ SPEC CHECKSUMS:
React-jsi: 4f35c1a2273d193a80c1c3831c808413840c260c
React-jsiexecutor: de1c37cf59ae9adcbf2be82eea0e090dc3f3205e
React-jsinspector: b76c4e84a7833bb4c90549d59ed53ec299ff912b
react-native-background-timer: 0d34748e53a972507c66963490c775321a88f6f2
react-native-background-timer: e0384ea2fa5a98f67f84f9c4dc274260ddd674ed
react-native-calendar-events: 1442fad71a00388f933cfa25512588fec300fcf8
react-native-keep-awake: eba3137546b10003361b37c761f6c429b59814ae
react-native-netinfo: 8d8db463bcc5db66a8ac5c48a7d86beb3b92f61a
react-native-webrtc: 9268ae9a2bc9730796b0968d012327e92c392adf
react-native-webview: 4dbc1d2a4a6b9c5e9e723c62651917aa2b5e579e
react-native-webview: 6ee7868ca8eba635dbf7963986d1ab7959da0391
React-RCTActionSheet: b72ddbfbe15b44ce691d128e4b582f4bb9abb540
React-RCTAnimation: cfaefba5024499d336b76ab850e6bd33b232b5e3
React-RCTBlob: c427e643bef82999deeab97489ba43298ecfbe24
@@ -589,6 +576,6 @@ SPEC CHECKSUMS:
RNWatch: a5320c959c75e72845c07985f3e935e58998f1d3
Yoga: 7b4209fda2441f99d54dd6cf4c82b094409bb68f
PODFILE CHECKSUM: 082858daebbe170e7a490de433e7f2a99e0c3701
PODFILE CHECKSUM: 224e84629bf45ae487c4ebc66faf33ec8304fb67
COCOAPODS: 1.9.1
COCOAPODS: 1.9.3

View File

@@ -20,8 +20,6 @@
#import "Types.h"
#import "ViewController.h"
@import Crashlytics;
@import Fabric;
@import Firebase;
@import JitsiMeet;
@@ -48,10 +46,11 @@
}];
// Initialize Crashlytics and Firebase if a valid GoogleService-Info.plist file was provided.
if ([FIRUtilities appContainsRealServiceInfoPlist] && ![jitsiMeet isCrashReportingDisabled]) {
NSLog(@"Enabling Crashlytics and Firebase");
if ([FIRUtilities appContainsRealServiceInfoPlist]) {
NSLog(@"Enabling Firebase");
[FIRApp configure];
[Fabric with:@[[Crashlytics class]]];
// Crashlytics defaults to disabled wirth the FirebaseCrashlyticsCollectionEnabled Info.plist key.
[[FIRCrashlytics crashlytics] setCrashlyticsCollectionEnabled:![jitsiMeet isCrashReportingDisabled]];
}
[jitsiMeet application:application didFinishLaunchingWithOptions:launchOptions];

View File

@@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>20.4.0</string>
<string>20.5.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
@@ -66,10 +66,10 @@
<string>See your scheduled meetings in the app.</string>
<key>NSCameraUsageDescription</key>
<string>Participate in meetings with video.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string></string>
<key>NSMicrophoneUsageDescription</key>
<string>Participate in meetings with voice.</string>
<key>NSLocalNetworkUsageDescription</key>
<string>Local network is used for establishing Peer-to-Peer connections.</string>
<key>NSUserActivityTypes</key>
<array>
<string>org.jitsi.JitsiMeet.ios.conference</string>
@@ -99,7 +99,7 @@
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>firebase_crashlytics_collection_enabled</key>
<key>FirebaseCrashlyticsCollectionEnabled</key>
<string>false</string>
</dict>
</plist>

View File

@@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>20.4.0</string>
<string>20.5.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>UISupportedInterfaceOrientations</key>

View File

@@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>20.4.0</string>
<string>20.5.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>CLKComplicationPrincipalClass</key>

View File

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

View File

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

View File

@@ -234,7 +234,7 @@
"micPermissionDeniedError": "Die Berechtigung zur Verwendung des Mikrofons wurde nicht erteilt. Sie können trotzdem an der Konferenz teilnehmen, aber die anderen Teilnehmer können Sie nicht hören. Verwenden Sie die Kamera-Schaltfläche in der Adressleiste, um die Berechtigungen zu erteilen.",
"micUnknownError": "Das Mikrofon kann aus einem unbekannten Grund nicht verwendet werden.",
"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}} stummschaten?",
"muteEveryoneElseTitle": "Alle außer {{whom}} stummschalten?",
"muteEveryoneDialog": "Wollen Sie wirklich alle stummschalten? Sie können deren Stummschaltung nicht mehr beenden, aber sie können ihre Stummschaltung jederzeit selbst beenden.",
"muteEveryoneTitle": "Alle stummschalten?",
"muteEveryoneSelf": "sich selbst",

View File

@@ -4,7 +4,7 @@
"addContacts": "Inviter vos contacts",
"copyInvite": "Copier l'invitation à la réunion",
"copyLink": "Copier le lien de la réunion",
"copyStream": "Copier le lien de diffision en direct",
"copyStream": "Copier le lien de diffusion en direct",
"countryNotSupported": "Cette destination n'est pas actuellement supportée.",
"countryReminder": "Appel hors des États-Unis ? Veuillez débuter par le code du pays !",
"defaultEmail": "Votre email par défaut",
@@ -139,6 +139,9 @@
"description": "Rien ne s'est passé ? Nous avons essayé de lancer votre réunion dans l'application de bureau {{app}}. Essayez à nouveau ou lancez-la dans l'application web {{app}}.",
"descriptionWithoutWeb": "Rien ne s'est passé ? Nous avons essayé de démarrer votre réunion dans l'application bureau {{app}}.",
"downloadApp": "Télécharger l'application",
"ifDoNotHaveApp": "Si vous n'avez pas encore l'application:",
"ifHaveApp": "Si vous avez déjà installé l'application:",
"joinInApp": "Rejoindre la réunion en utilisant l'application",
"launchWebButton": "Lancer dans le navigateur",
"openApp": "Continuer vers l'application",
"title": "Lancement de votre réunion dans {{app}} en cours...",
@@ -194,7 +197,7 @@
"done": "Terminé",
"enterDisplayName": "Merci de saisir votre nom ici",
"error": "Erreur",
"grantModeratorDialog": "Êtes vous sûr de vouloir rendre ce participant modérateur?",
"grantModeratorDialog": "Êtes-vous sûr de vouloir rendre ce participant modérateur?",
"grantModeratorTitle": "Nommer modérateur",
"externalInstallationMsg": "Vous devez installer notre extension de partage de bureau.",
"externalInstallationTitle": "Extension requise",
@@ -230,7 +233,7 @@
"micUnknownError": "Vous ne pouvez pas utiliser le microphone pour une raison inconnue.",
"muteEveryoneElseDialog": "Une fois leur micro coupé, vous ne pourrez plus le réactiver, mais ils pourront l'activer par eux-mêmes à tout moment.",
"muteEveryoneElseTitle": "Couper le micro de tout le monde sauf de {{whom}} ?",
"muteEveryoneDialog": "Etes-vous sûr de vouloir couper les micros de tout le monde ? Vous ne pourrez plus réactiver leur micro, mais ils pourront l'activer par eux-mêmes à tout moment.",
"muteEveryoneDialog": "Êtes-vous sûr de vouloir couper les micros de tout le monde ? Vous ne pourrez plus réactiver leur micro, mais ils pourront l'activer par eux-mêmes à tout moment.",
"muteEveryoneTitle": "Couper le micro de tout le monde ?",
"muteEveryoneSelf": "vous",
"muteEveryoneStartMuted": "Tout le monde démarre avec le micro coupé",
@@ -697,6 +700,8 @@
"hangup": "Quitter",
"help": "Aide",
"invite": "Inviter des participants",
"lobbyButtonDisable": "Désactiver le contrôle des participant·e·s",
"lobbyButtonEnable": "Activer le contrôle des participant·e·s",
"login": "Connexion",
"logout": "Déconnexion",
"lowerYourHand": "Baisser la main",

File diff suppressed because it is too large Load Diff

View File

@@ -1,60 +1,86 @@
{
"addPeople": {
"add": "초대",
"countryNotSupported": "아직 해당 지역을 지원하지 않습니다",
"addContacts": "연락처로 초대하세요",
"copyInvite": "호의 초대 복사",
"copyLink": "회의 링크 복사",
"copyStream": "라이브 스트리밍 링크 복사",
"countryNotSupported": "아직 해당 지역을 지원하지 않습니다.",
"countryReminder": "미국 이외의 지역으로 전화하시겠습니까? 국가 번호로 시작해야합니다!",
"disabled": "사람들을 초대 할 수 없습니다",
"failedToAdd": "",
"footerText": "",
"defaultEmail": "기본 이메일",
"disabled": "사람들을 초대 할 수 없습니다.",
"failedToAdd": "참가자를 추가하지 못했습니다.",
"footerText": "전화 걸기가 비활성화되었습니다.",
"googleEmail": "Google 이메일",
"inviteMoreHeader": "회의에 혼자 참여하고 있습니다.",
"inviteMoreMailSubject": "{{appName}} 회의에 참여하세요",
"inviteMorePrompt": "더 많은 사람을 초대하세요",
"linkCopied": "링크가 클립보드에 복사되었습니다.",
"loading": "사람 및 전화번호 검색",
"loadingNumber": "전화번호 확인 중",
"loadingPeople": "초대할 사람 찾기",
"noResults": "일치하는 검색 결과 없음",
"noValidNumbers": "전화 번호를 입력하십시오.",
"outlookEmail": "Outlook 이메일",
"searchNumbers": "전화번호 추가",
"searchPeople": "인명 검색",
"searchPeopleAndNumbers": "인명 검색 또는 전화번호 추가",
"shareInvite": "회의 초대 공유",
"shareLink": "다른 사람을 초대하려면 회의 링크를 공유하세요.",
"shareStream": "라이브 스트리밍 링크 공유",
"telephone": "전화: {{number}}",
"title": "이 회의에 사람들을 초대하십시오"
"title": "이 회의에 사람들을 초대하십시오",
"yahooEmail": "Yahoo 이메일"
},
"audioDevices": {
"bluetooth": "블루투스",
"headphones": "헤드폰",
"phone": "폰",
"speaker": "스피커"
"speaker": "스피커",
"none": "사용 가능한 오디오 장치가 없습니다."
},
"audioOnly": {
"audioOnly": "음성 전용"
},
"calendarSync": {
"addMeetingURL": "",
"confirmAddLink": "",
"addMeetingURL": "회의 링크 추가",
"confirmAddLink": "이 이벤트에 Jitsi 링크를 추가 하시겠습니까?",
"error": {
"appConfiguration": "",
"generic": "",
"notSignedIn": ""
"appConfiguration": "캘린더가 제대로 구성되지 않았습니다.",
"generic": "오류가 발생했습니다. 캘린더 설정을 확인하거나 캘린더를 새로 고침 해보세요.",
"notSignedIn": "캘린더 이벤트를 보기 위해 인증하는 동안 오류가 발생했습니다. 캘린더 설정을 확인하고 다시 로그인하십시오."
},
"join": "",
"joinTooltip": "",
"join": "참여",
"joinTooltip": "회의에 참여하세요",
"nextMeeting": "다음 회의",
"noEvents": "",
"ongoingMeeting": "",
"noEvents": "예정된 예정된 이벤트가 없습니다.",
"ongoingMeeting": "진행중인 회의",
"permissionButton": "설정 열기",
"permissionMessage": "앱에 회의를 나열하려면 캘린더 권한이 필요합니다",
"refresh": "",
"today": ""
"refresh": "달력 새로고침",
"today": "오늘"
},
"chat": {
"error": "",
"messagebox": "",
"error": "오류 : 메시지가 전송되지 않았습니다. 이유 : {{error}}",
"fieldPlaceHolder": "메세지를 여기에 입력하세요",
"messagebox": "메시지 입력",
"messageTo": "{{recipient}}에게 보내는 비공개 메시지",
"noMessagesMessage": "아직 회의에 메시지가 없습니다. 여기서 대화를 시작하세요!",
"nickname": {
"popover": "닉네임을 선택하세요",
"title": ""
"title": "채팅에서 사용할 닉네임을 입력하세요"
},
"title": ""
"privateNotice": "{{recipient}}에게 보내는 비공개 메시지",
"title": "채팅",
"you": "당신"
},
"chromeExtensionBanner": {
"installExtensionText": "Google 캘린더 및 Office 365 확장 프로그램을 설치합니다.",
"buttonText": "Chrome 확장 프로그램을 설치합니다.",
"dontShowAgain": "다시 보지 않기"
},
"connectingOverlay": {
"joiningRoom": ""
"joiningRoom": "회의에 연결 중 ..."
},
"connection": {
"ATTACHED": "첨부",
@@ -66,7 +92,10 @@
"DISCONNECTED": "연결 끊김",
"DISCONNECTING": "연결 종료 중",
"ERROR": "에러",
"RECONNECTING": "네트워크 문제가 발생했습니다. 다시 연결 중..."
"RECONNECTING": "네트워크 문제가 발생했습니다. 다시 연결 중...",
"GET_SESSION_ID_ERROR": "세션 ID 가져 오기 오류 : {{code}}",
"GOT_SESSION_ID": "세션 ID를 가져 오는 중 ... 완료",
"LOW_BANDWIDTH": "대역폭을 절약하기 위해 {{displayName}}의 동영상이 중지되었습니다."
},
"connectionindicator": {
"address": "주소:",
@@ -95,15 +124,18 @@
"turn": " (turn)"
},
"dateUtils": {
"earlier": "",
"today": "",
"yesterday": ""
"earlier": "일찍이",
"today": "오늘",
"yesterday": "어제"
},
"deepLinking": {
"appNotInstalled": "중계 서비스에 참여하려면 모바일 앱 설치가 필요합니다",
"appNotInstalled": "회의에 참여하려면 모바일 앱 설치가 필요합니다",
"description": "{{app}} 데스크톱 앱에서 회의를 시작했습니다. {{app}} 웹 응용 프로그램에서 다시 시도하거나 실행하십시오.",
"descriptionWithoutWeb": "",
"downloadApp": "앱 다운로드",
"ifDoNotHaveApp": "앱이 설치되지 않은 경우:",
"ifHaveApp": "앱이 설치되어 있는 경우:",
"joinInApp": "앱을 사용하여 회의에 참여하세요.",
"launchWebButton": "웹에서 실행",
"openApp": "방으로 이동하기",
"title": "{{app}}에서 회의 시작…",
@@ -126,8 +158,9 @@
"accessibilityLabel": {
"liveStreaming": "실시간 스트리밍:"
},
"add": "추가",
"allow": "허락",
"alreadySharedVideoMsg": "",
"alreadySharedVideoMsg": "다른 참가자가 이미 비디오를 공유하고 있습니다. 이 회의는 한 번에 하나의 공유 비디오 만 허용합니다.",
"alreadySharedVideoTitle": "한 번에 하나의 공유 비디오 만 허용됩니다",
"applicationWindow": "응용 프로그램 창",
"Back": "뒤로가기",
@@ -145,64 +178,64 @@
"conferenceReloadMsg": "문제를 해결하려고 노력하고 있습니다. {{seconds}} 초 안에 다시 연결중입니다.",
"conferenceReloadTitle": "불행하게도 문제가 발생했습니다",
"confirm": "확인",
"confirmNo": "",
"confirmYes": "",
"confirmNo": "아니요",
"confirmYes": "",
"connectError": "죄송합니다. 문제가 발생하여 회의에 연결할 수 없습니다",
"connectErrorWithMsg": "죄송합니다. 뭔가 잘못되어 회의에 연결할 수 없습니다: {{msg}}",
"connecting": "연결 중",
"contactSupport": "지원 연락처",
"copy": "복사",
"dismiss": "",
"displayNameRequired": "",
"displayNameRequired": "당신의 이름은 무엇입니까?",
"done": "완료",
"enterDisplayName": "",
"enterDisplayName": "당신의 이름을 입력해주세요.",
"error": "에러",
"externalInstallationMsg": "데스크톱 공유 확장 프로그램을 설치해야합니다",
"externalInstallationTitle": "확장 프로그램이 필요합니다",
"goToStore": "웹 스토어로 이동",
"gracefulShutdown": "서비스는 현재 유지 관리를 위해 중단되었습니다. 나중에 다시 시도 해주십시오.",
"IamHost": "내가 호스트",
"incorrectRoomLockPassword": "",
"incorrectRoomLockPassword": "잘못된 비밀번호",
"incorrectPassword": "잘못된 사용자 이름 또는 비밀번호",
"inlineInstallationMsg": "데스크톱 공유 확장 프로그램을 설치해야합니다",
"inlineInstallExtension": "지금 설치",
"internalError": "죄송합니다. 뭔가 잘못 됐습니다. 다음 오류가 발생했습니다: {{error}}",
"internalErrorTitle": "내부 에러",
"kickMessage": "",
"kickParticipantButton": "",
"kickParticipantDialog": "",
"kickParticipantTitle": "",
"kickTitle": "",
"kickMessage": "자세한 내용은 {{participantDisplayName}}에 문의하세요.",
"kickParticipantButton": "추방",
"kickParticipantDialog": "이 참가자를 정말 추방 하시겠습니까?",
"kickParticipantTitle": "이 참가자를 추방 하시겠습니까?",
"kickTitle": "{{participantDisplayName}} 님이 회의에서 퇴장했습니다.",
"liveStreaming": "실시간 스트리밍",
"liveStreamingDisabledForGuestTooltip": "",
"liveStreamingDisabledTooltip": "",
"liveStreamingDisabledForGuestTooltip": "게스트는 라이브 스트리밍을 시작할 수 없습니다.",
"liveStreamingDisabledTooltip": "라이브 스트림 시작이 비활성화되었습니다.",
"lockMessage": "회의를 비공개하지 못했습니다",
"lockRoom": "",
"lockRoom": "회의 추가 $t(lockRoomPasswordUppercase)",
"lockTitle": "비공개 실패",
"logoutQuestion": "로그 아웃하고 컨퍼런스를 중지하시겠습니까?",
"logoutTitle": "로그아웃",
"maxUsersLimitReached": "",
"maxUsersLimitReachedTitle": "",
"maxUsersLimitReached": "회의의 최대 참가자 수에 도달했습니다. 회의 소유자에게 연락하거나 나중에 다시 시도하십시오!",
"maxUsersLimitReachedTitle": "최대 참가자 수에 도달했습니다.",
"micConstraintFailedError": "마이크가 필요한 제약 조건 중 일부를 충족하지 못합니다",
"micNotFoundError": "마이크를 찾을 수 없습니다",
"micNotSendingData": "",
"micNotSendingDataTitle": "",
"micNotSendingData": "컴퓨터의 설정으로 이동하여 마이크 음소거를 해제하고 레벨을 조정하세요.",
"micNotSendingDataTitle": "시스템 설정에 의해 마이크가 음소거되었습니다.",
"micPermissionDeniedError": "마이크를 사용할 수있는 권한을 부여하지 않았습니다. 회의에 계속 참여할 수는 있지만 다른 사람들은 듣지 않습니다. 검색 주소창의 카메라 버튼을 사용하여 문제를 해결하십시오.",
"micUnknownError": "알 수 없는 이유로 마이크를 사용할 수 없습니다",
"muteParticipantBody": "당신이 다른 사람들의 음소거를 해제 할 수는 없지만 언제든지 다른 사람들은 스스로 음소거를 해제할 수 있습니다.",
"muteParticipantButton": "음소거",
"muteParticipantDialog": "",
"muteParticipantTitle": "",
"muteParticipantTitle": "이 참가자를 음소거 하시겠습니까?",
"Ok": "확인",
"passwordLabel": "",
"passwordNotSupported": "미팅 비밀번호 설정은 지원되지 않습니다",
"passwordNotSupportedTitle": "",
"passwordRequired": "",
"passwordLabel": "잠긴 회의 입니다. 회의에 참여하려면 비밀번호를 입력하세요.",
"passwordNotSupported": "회의 비밀번호 설정은 지원되지 않습니다",
"passwordNotSupportedTitle": "비밀번호 미지원",
"passwordRequired": "비밀번호 필수",
"popupError": "브라우저가이 사이트의 팝업 창을 차단하고 있습니다. 브라우저의 보안 설정에서 팝업을 활성화하고 다시 시도하십시오.",
"popupErrorTitle": "팝업 차단됨",
"recording": "레코딩",
"recordingDisabledForGuestTooltip": "",
"recordingDisabledTooltip": "",
"recordingDisabledForGuestTooltip": "게스트는 녹음을 시작할 수 없습니다.",
"recordingDisabledTooltip": "녹화이 비활성화 되었습니다.",
"rejoinNow": "지금 재가입",
"remoteControlAllowedMessage": "{{user}}이(가) 원격 제어 요청을 수락했습니다",
"remoteControlDeniedMessage": "{{user}}이(가) 원격 제어 요청을 거부했습니다",
@@ -212,25 +245,30 @@
"remoteControlStopMessage": "원격 제어 세션이 종료되었습니다",
"remoteControlTitle": "원격 데스크탑 컨트롤",
"Remove": "제거",
"removePassword": "",
"removePassword": "비밀번호 제거",
"removeSharedVideoMsg": "공유한 동영상을 삭제하시겠습니까?",
"removeSharedVideoTitle": "공유된 동영상 삭제",
"reservationError": "예약 시스템 오류",
"reservationErrorMsg": "오류 코드: {{code}}, 메시지: {{msg}}",
"retry": "재시도",
"screenSharingAudio": "오디오 공유",
"screenSharingFailedToInstall": "죄송합니다. 화면 공유 확장 프로그램을 설치하지 못했습니다.",
"screenSharingFailedToInstallTitle": "화면 공유 확장 프로그램을 설치하지 못했습니다",
"screenSharingFirefoxPermissionDeniedError": "화면을 공유하는 동안 문제가 발생했습니다. 그렇게 할 수 있는 권한을 부여했는지 확인하십시오.",
"screenSharingFirefoxPermissionDeniedTitle": "죄송합니다. 화면 공유를 시작할 수 없었습니다!",
"screenSharingPermissionDeniedError": "죄송합니다. 화면 공유 확장 권한으로 문제가 발생했습니다. 다시 로드하고 재시도하십시오.",
"sendPrivateMessage": "최근에 비공개 메시지를 받았습니다. 비공개로 답장을 보내시겠습니까, 아니면 그룹에 메시지를 보내시겠습니까?",
"sendPrivateMessageCancel": "그룹에 보내기",
"sendPrivateMessageOk": "비공개로 보내기",
"sendPrivateMessageTitle": "비공개로 보낼까요?",
"serviceUnavailable": "서비스를 사용할 수 없음",
"sessTerminated": "통화 종료",
"Share": "공유",
"shareVideoLinkError": "올바른 YouTube 링크를 제공하십시오",
"shareVideoTitle": "비디오 공유",
"shareYourScreen": "화면공유",
"shareYourScreenDisabled": "",
"shareYourScreenDisabledForGuest": "",
"shareYourScreenDisabled": "화면 공유가 비활성화 되었습니다.",
"shareYourScreenDisabledForGuest": "게스트는 화면을 공유 할 수 없습니다.",
"startLiveStreaming": "라이브 스트리밍 시작",
"startRecording": "레코딩 시작",
"startRemoteControlErrorMessage": "원격 제어 세션을 시작하는 동안 오류가 발생했습니다",
@@ -245,17 +283,20 @@
"tokenAuthFailed": "죄송합니다. 통화에 참여하실 수 없습니다.",
"tokenAuthFailedTitle": "인증 실패",
"transcribing": "",
"unlockRoom": "",
"unlockRoom": "회의 비밀번호 제거",
"userPassword": "사용자 비밀번호",
"WaitForHostMsg": "",
"WaitForHostMsgWOk": "",
"WaitForHostMsg": "<b>{{room}}</b> 회의가 시작되지 않았습니다. 호스트 인 경우 인증하십시오. 그렇지 않으면 호스트가 도착할 때까지 기다리십시오.",
"WaitForHostMsgWOk": "<b>{{room}}</b> 회의가 아직 시작되지 않았습니다. 호스트 인 경우 확인을 눌러 인증하십시오. 그렇지 않으면 호스트가 도착할 때까지 기다리십시오.",
"WaitingForHost": "호스트를 기다리는 중입니다…",
"Yes": "",
"Yes": "",
"yourEntireScreen": "전체 화면"
},
"dialOut": {
"statusMessage": "지금은 {{status}}입니다"
},
"documentSharing": {
"title": "문서 공유"
},
"feedback": {
"average": "보통",
"bad": "나쁨",
@@ -266,49 +307,49 @@
"veryGood": "매우 좋음"
},
"incomingCall": {
"answer": "",
"audioCallTitle": "",
"decline": "",
"productLabel": "",
"videoCallTitle": ""
"answer": "응답",
"audioCallTitle": "수신 전화",
"decline": "거절",
"productLabel": "Jitsi Meet에서",
"videoCallTitle": "수신 화상 전화"
},
"info": {
"accessibilityLabel": "",
"addPassword": "",
"cancelPassword": "",
"accessibilityLabel": "정보 보기",
"addPassword": "$t(lockRoomPassword) 추가",
"cancelPassword": "$t(lockRoomPassword) 취소",
"conferenceURL": "링크:",
"country": "지역",
"dialANumber": "",
"dialANumber": "회의에 참여하려면이 번호 중 하나를 누른 다음 PIN을 입력하십시오.",
"dialInConferenceID": "PIN:",
"dialInNotSupported": "죄송합니다. 현재 전화를 걸 수 없습니다.",
"dialInNumber": "Dial-in:",
"dialInSummaryError": "",
"dialInSummaryError": "지금 전화 접속 정보를 가져 오는 중에 오류가 발생했습니다. 나중에 다시 시도하십시오.",
"dialInTollFree": "",
"genericError": "일반적인 오류가 발생했습니다",
"inviteLiveStream": "이 회의의 실시간 스트림을 보려면이 링크를 클릭하십시오: {{url}}",
"invitePhone": "",
"invitePhoneAlternatives": "",
"inviteURLFirstPartGeneral": "",
"inviteURLFirstPartPersonal": "",
"inviteURLSecondPart": "",
"inviteURLFirstPartGeneral": "회의에 초대되었습니다.",
"inviteURLFirstPartPersonal": "{{name}}이 회의에 초대하였습니다.\n",
"inviteURLSecondPart": "\n회의에 참여하기:\n{{url}}\n",
"liveStreamURL": "실시간 스트리밍:",
"moreNumbers": "더 많은 번호",
"noNumbers": "전화 접속 번호 없음",
"noPassword": "없음",
"noRoom": "전화 접속이 가능한 방을 지정하지 않았습니다",
"numbers": "전화 접속 번호",
"password": "",
"password": "비밀번호",
"title": "공유",
"tooltip": "링크 공유 및 회의에 대한 정보",
"label": ""
"label": "회의 정보"
},
"inviteDialog": {
"alertText": "",
"alertText": "일부 참가자를 초대하지 못했습니다.",
"header": "초대",
"searchCallOnlyPlaceholder": "",
"searchPeopleOnlyPlaceholder": "",
"searchPlaceholder": "",
"send": ""
"searchCallOnlyPlaceholder": "전화 번호 입력",
"searchPeopleOnlyPlaceholder": "참가자 검색",
"searchPlaceholder": "참가자 또는 전화 번호",
"send": "전송"
},
"inlineDialogFailure": {
"msg": "약간의 문제가 있습니다",
@@ -321,7 +362,7 @@
"focusRemote": "다른 발신자의 동영상에 포커스",
"fullScreen": "전체화면 표시 또는 종료",
"keyboardShortcuts": "키보드 단축키",
"localRecording": "",
"localRecording": "로컬 녹음 컨트롤 표시 또는 숨기기",
"mute": "마이크 음소거 또는 음소거 해제",
"pushToTalk": "대화 요청",
"raiseHand": "말하기 요청/해제",
@@ -341,24 +382,26 @@
"enterStreamKey": "YouTube 실시간 스트리밍 키를 입력하십시오",
"error": "실시간 스트리밍에 실패했습니다. 다시 시도하십시오.",
"errorAPI": "YouTube 방송에 액세스하는 중에 오류가 발생했습니다. 다시 로그인하십시오.",
"errorLiveStreamNotEnabled": "",
"expandedOff": "",
"expandedOn": "",
"expandedPending": "",
"errorLiveStreamNotEnabled": "{{email}}에 의해 라이브 스트리밍이 활성화되지 않았습니다. 라이브 스트리밍을 활성화하거나 라이브 스트리밍이 활성화 된 계정으로 로그인하십시오.",
"expandedOff": "라이브 스트리밍이 중지되었습니다",
"expandedOn": "현재 회의가 YouTube로 스트리밍되고 있습니다.",
"expandedPending": "라이브 스트리밍이 시작됩니다 ...",
"failedToStart": "실시간 스트리밍 시작 실패",
"getStreamKeyManually": "",
"invalidStreamKey": "",
"getStreamKeyManually": "실시간 스트림을 가져올 수 없습니다. YouTube에서 실시간 스트림 키를 받아보세요.",
"invalidStreamKey": "라이브 스트림 키가 잘못되었을 수 있습니다.",
"off": "실시간 스트리밍이 중지됨",
"on": "실시간 스트리밍",
"pending": "실시간 스트리밍 시작…",
"serviceName": "실시간 스트리밍 서비스",
"signedInAs": "",
"signedInAs": "현재 다음 계정으로 로그인되어 있습니다.",
"signIn": "Google로 로그인",
"signInCTA": "YouTube에서 로그인하거나 실시간 스트리밍 키를 입력하십시오",
"signOut": "",
"signOut": "로그아웃",
"start": "실시간 스트리밍 시작",
"streamIdHelp": "도움말?",
"unavailableTitle": "실시간 스트리밍을 사용할 수 없음"
"unavailableTitle": "실시간 스트리밍을 사용할 수 없음",
"youtubeTerms": "YouTube 서비스 약관",
"googlePrivacyPolicy": "Google 개인 정보 보호 정책"
},
"localRecording": {
"clientState": {
@@ -381,50 +424,50 @@
"notModerator": ""
},
"moderator": "",
"no": "",
"no": "아니요",
"participant": "",
"participantStats": "",
"sessionToken": "",
"start": "레코딩 시작",
"stop": "레코딩 종료",
"yes": ""
"yes": ""
},
"lockRoomPassword": "패스워드",
"lockRoomPasswordUppercase": "패스워드",
"me": "Me",
"lockRoomPassword": "비밀번호",
"lockRoomPasswordUppercase": "비밀번호",
"me": "",
"notify": {
"connectedOneMember": "",
"connectedThreePlusMembers": "",
"connectedTwoMembers": "",
"connectedOneMember": "{{name}}님이 회의에 참여했습니다.",
"connectedThreePlusMembers": "{{name}}님 외 {{count}}명이 회의에 참여했습니다.",
"connectedTwoMembers": "{{first}}님과 {{second}}님이 회의에 참여했습니다.",
"disconnected": "연결이 끊김",
"focus": "컨퍼런스 포커스",
"focusFail": "{{component}}을 사용할 수 없음 - {{ms}} 초 후에 다시 시도하십시오",
"grantedTo": "{{to}}에게 방장 권한이 부여되었습니다!",
"invitedOneMember": "",
"invitedThreePlusMembers": "",
"invitedTwoMembers": "",
"kickParticipant": "",
"me": "",
"invitedOneMember": "{{name}}님이 초대되었습니다.",
"invitedThreePlusMembers": "{{name}}님 외 {{count}}명이 초대되었습니다.",
"invitedTwoMembers": "{{first}}님과 {{second}}님이 초대되었습니다.",
"kickParticipant": "{{kicker}}님이 {{kicked}}님을 추방했습니다.",
"me": "",
"moderator": "방장 권한이 부여되었습니다!",
"muted": "음소거로 대화가 시작되었습니다",
"mutedTitle": "음소거 상태입니다!",
"mutedRemotelyTitle": "",
"mutedRemotelyDescription": "",
"passwordRemovedRemotely": "",
"passwordSetRemotely": "",
"raisedHand": "",
"mutedRemotelyTitle": "{{participantDisplayName}}에 의해 음소거되었습니다!",
"mutedRemotelyDescription": "말할 준비가되면 언제든지 음소거를 해제 할 수 있습니다.",
"passwordRemovedRemotely": "다른 참가자가 $t(lockRoomPasswordUppercase)를 제거했습니다.",
"passwordSetRemotely": "다른 참가자가 $t(lockRoomPasswordUppercase)를 설정했습니다.",
"raisedHand": "{{name}}님이 말하고 싶어합니다.",
"somebody": "누군가",
"startSilentTitle": "",
"startSilentDescription": "",
"startSilentTitle": "오디오 출력없이 참여했습니다!",
"startSilentDescription": "오디오를 사용하려면 회의에 다시 참여하세요.",
"suboptimalExperienceDescription": "{{appName}}에 대한 귀하의 경험이 없으시다면 <a href='{{recommendedBrowserPageLink}}' target='_blank'>완벽하게 지원되는 브라우저</a> 중 하나를 사용해보십시오.",
"suboptimalExperienceTitle": "브라우저 경고",
"unmute": "",
"newDeviceCameraTitle": "",
"newDeviceAudioTitle": "",
"newDeviceAction": ""
"unmute": "음소거 해제",
"newDeviceCameraTitle": "새 카메라 감지",
"newDeviceAudioTitle": "새 오디오 장치 감지",
"newDeviceAction": "사용"
},
"passwordSetRemotely": "",
"passwordDigitsOnly": "",
"passwordSetRemotely": "다른 참가자가 설정",
"passwordDigitsOnly": "최대 {{number}} 자리",
"poweredby": "powered by",
"presenceStatus": {
"busy": "바쁨",
@@ -447,27 +490,27 @@
"title": "프로필"
},
"recording": {
"authDropboxText": "",
"availableSpace": "",
"authDropboxText": "Dropbox에 업로드",
"availableSpace": "사용 가능한 공간 : {{spaceLeft}}MB (약 {{duration}}분 녹화)",
"beta": "베타",
"busy": "레코딩 자원을 확보하고 있습니다. 몇 분 후에 다시 시도하십시오.",
"busyTitle": "모든 레코더가 현재 사용 중입니다",
"error": "레코딩이 실패했습니다. 다시 시도하십시오.",
"expandedOff": "레코딩이 중지됨",
"expandedOn": "",
"expandedPending": "",
"expandedOn": "회의가 현재 녹화 중입니다.",
"expandedPending": "녹화가 시작됩니다 ...",
"failedToStart": "레코딩을 시작하지 못했습니다",
"fileSharingdescription": "",
"fileSharingdescription": "회의 참가자와 녹음 공유",
"live": "라이브",
"loggedIn": "",
"loggedIn": "{{userName}}으로 로그인했습니다.",
"off": "레코딩이 중지됨",
"on": "레코딩",
"pending": "참석할 멤버를 기다리는 중입니다…",
"rec": "REC",
"serviceDescription": "",
"rec": "녹음",
"serviceDescription": "녹음은 녹음 서비스에 의해 저장됩니다.",
"serviceName": "레코딩 서비스",
"signIn": "",
"signOut": "",
"signIn": "로그인",
"signOut": "로그아웃",
"unavailable": "죄송합니다. {{serviceName}}은 현재 사용할 수 없습니다. 저희는 문제를 해결하기 위해 노력하고 있습니다. 나중에 다시 시도 해주십시오.",
"unavailableTitle": "레코딩을 사용할 수 없습니다"
},
@@ -476,18 +519,18 @@
},
"settings": {
"calendar": {
"about": "",
"about": "{{appName}} 캘린더 통합은 예정된 일정을 읽을 수 있도록 캘린더에 안전하게 액세스하는 데 사용됩니다.",
"disconnect": "연결 끊김",
"microsoftSignIn": "",
"signedIn": "",
"title": ""
"microsoftSignIn": "Microsoft로 로그인",
"signedIn": "현재 {{email}}의 캘린더 일정에 액세스하고 있습니다. 캘린더 이벤트 액세스를 중지하려면 아래 연결 해제 버튼을 클릭하세요.",
"title": "캘린더"
},
"devices": "",
"devices": "장치",
"followMe": "모두 나와 같은 설정 상태로",
"language": "",
"loggedIn": "",
"moderator": "",
"more": "",
"language": "언어",
"loggedIn": "{{name}}으로 로그인",
"moderator": "마이크",
"more": "더보기",
"name": "이름",
"noDevice": "없음",
"selectAudioOutput": "오디오 출력",
@@ -495,26 +538,28 @@
"selectMic": "오디오",
"startAudioMuted": "모두가 음소거를 시작합니다",
"startVideoMuted": "모두가 비디오 비활성화로 시작합니다",
"title": "세티"
"title": "설정"
},
"settingsView": {
"advanced": "고급",
"alertOk": "확인",
"alertCancel": "취소",
"alertTitle": "경고",
"alertURLText": "입력된 서버 URL이 잘못되었습니다",
"buildInfoSection": "",
"buildInfoSection": "빌드 정보",
"conferenceSection": "회의",
"displayName": "유저이름",
"email": "이메일",
"header": "세티",
"header": "설정",
"profileSection": "프로필",
"serverURL": "서버 URL",
"startWithAudioMuted": "오디오 음소거 상태로 시작",
"startWithVideoMuted": "비디오 비활성화 상태로 시작",
"version": ""
"version": "버전"
},
"share": {
"dialInfoText": "",
"mainText": ""
"mainText": "회의에 참여하려면 다음 링크를 클릭하십시오.\n{{roomUrl}}"
},
"speaker": "스피커",
"speakerStats": {
@@ -561,11 +606,11 @@
"sharedvideo": "",
"shareRoom": "",
"shareYourScreen": "",
"shortcuts": "단축키 토그",
"shortcuts": "단축키 전환",
"show": "",
"speakerStats": "",
"tileView": "",
"toggleCamera": "카메라 토ㄱ",
"toggleCamera": "카메라 전환",
"videomute": "",
"videoblur": ""
},
@@ -575,54 +620,58 @@
"audioRoute": "음성 장비 선택하기",
"authenticate": "인증 중",
"callQuality": "품질 설정하기",
"chat": "",
"closeChat": "",
"documentClose": "",
"documentOpen": "",
"chat": "대화 열기/닫기",
"closeChat": "대화 닫기",
"documentClose": "문서 공유 닫기",
"documentOpen": "문서 공유 열기",
"download": "앱 다운로드",
"enterFullScreen": "전체화면 보기",
"enterTileView": "",
"enterTileView": "타일보기 시작",
"exitFullScreen": "전체화면 취소",
"exitTileView": "",
"exitTileView": "타일보기 종료",
"feedback": "피드백 남기기",
"hangup": "",
"invite": "",
"login": "",
"hangup": "떠나기",
"invite": "초대",
"login": "로그인",
"logout": "로그아웃",
"lowerYourHand": "",
"lowerYourHand": "손을 내려주세요",
"moreActions": "추가 액션",
"mute": "마이크",
"openChat": "",
"moreOptions": "옵션 더보기",
"mute": "음소거 설정/해제",
"muteEveryone": "모두 음소거",
"openChat": "대화 열기",
"pip": "",
"profile": "",
"privateMessage": "비공개 메시지 보내기",
"profile": "프로필 수정",
"raiseHand": "말하기 요청/해제",
"raiseYourHand": "",
"Settings": "세티",
"sharedvideo": "",
"shareRoom": "",
"shortcuts": "",
"raiseYourHand": "손 들어주세요",
"Settings": "설정",
"sharedvideo": "YouTube 비디오 공유",
"shareRoom": "초대하기",
"shortcuts": "단축키보기",
"speakerStats": "접속자 통계",
"startScreenSharing": "",
"startSubtitles": "",
"stopScreenSharing": "",
"stopSubtitles": "",
"stopSharedVideo": "",
"talkWhileMutedPopup": "",
"tileViewToggle": "",
"toggleCamera": "카메라 토ㄱ",
"videomute": "",
"startvideoblur": "",
"stopvideoblur": ""
"startScreenSharing": "화면 공유 시작",
"startSubtitles": "자막 시작",
"stopScreenSharing": "화면 공유 중지",
"stopSubtitles": "자막 중지",
"stopSharedVideo": "UouTube 비디오 공유 중지",
"talkWhileMutedPopup": "음소거 상태입니다.",
"tileViewToggle": "타일뷰 전환",
"toggleCamera": "카메라 전환",
"videomute": "카메라 시작/중지",
"startvideoblur": "내 배경을 흐리게",
"stopvideoblur": "배경 흐림 비활성화"
},
"transcribing": {
"ccButtonTooltip": "",
"ccButtonTooltip": "자막 시작/종료",
"error": "레코딩이 실패했습니다. 다시 시도하십시오.",
"expandedLabel": "",
"failedToStart": "",
"labelToolTip": "",
"off": "",
"expandedLabel": "현재 스크립트 작성 중",
"failedToStart": "스크립트 작성을 시작하지 못했습니다.",
"labelToolTip": "회의가 기록되고 있습니다.",
"off": "스크립트 작성이 중지되었습니다.",
"pending": "참석할 멤버를 기다리는 중입니다…",
"start": "",
"stop": "",
"start": "자막 표시 시작",
"stop": "자막 표시 중지",
"tr": ""
},
"userMedia": {
@@ -649,8 +698,8 @@
},
"videoStatus": {
"audioOnly": "오디오 전용",
"audioOnlyExpanded": "",
"callQuality": "",
"audioOnlyExpanded": "낮은 대역폭 모드에 있습니다. 이 모드에서는 오디오 및 화면 공유 만 수신합니다.",
"callQuality": "비디오 품질",
"hd": "HD",
"highDefinition": "고해상도",
"labelTooiltipNoVideo": "비디오 없음",
@@ -666,12 +715,12 @@
"domute": "음소거",
"flip": "플립",
"kick": "내보내기",
"moderator": "",
"mute": "",
"moderator": "중재자",
"mute": "참가자 음소거",
"muted": "음소거됨",
"remoteControl": "원격 제어",
"show": "",
"videomute": ""
"show": "화면에 표시",
"videomute": "참가자가 카메라를 중지했습니다."
},
"welcomepage": {
"accessibilityLabel": {
@@ -683,22 +732,33 @@
"audio": "음성",
"video": "비디오"
},
"calendar": "",
"connectCalendarButton": "",
"connectCalendarText": "",
"enterRoomTitle": "",
"calendar": "캘린더",
"connectCalendarButton": "캘린더를 연결하세요",
"connectCalendarText": "{{app}}에서 모든 회의를 보려면 캘린더를 연결하세요. 또한 캘린더에 {{provider}} 회의를 추가하고 클릭 한 번으로 시작하세요.",
"enterRoomTitle": "새 회의 시작",
"getHelp": "도움 받기",
"roomNameAllowedChars": "회의 이름은 이러한 문자를 포함 할 수 없습니다.: ?, &, :, ', \", %, #.",
"go": "계속",
"goSmall": "계속",
"join": "가입",
"info": "",
"info": "정보",
"privacy": "개인정보",
"recentList": "",
"recentListDelete": "",
"recentListEmpty": "",
"reducedUIText": "",
"recentList": "최근",
"recentListDelete": "삭제",
"recentListEmpty": "최근 목록이 현재 비어 있습니다. 팀과 채팅하면 여기에서 최근 회의를 모두 찾을 수 있습니다.",
"reducedUIText": "{{app}}에 오신 것을 환영합니다!",
"roomname": "방 이름 입력",
"roomnameHint": "",
"sendFeedback": "",
"roomnameHint": "참여하려는 방의 이름 또는 URL을 입력하십시오. 이름을 정하고 만나는 사람들에게 같은 이름을 입력하도록 알리면됩니다.",
"sendFeedback": "피드백 보내기",
"terms": "이용약관",
"title": ""
"title": "안전하고 모든 기능을 갖춘 완전 무료 화상 회의"
},
"lonelyMeetingExperience": {
"button": "초대하기",
"youAreAlone": "회의에 참여자가 없습니다."
},
"helpView": {
"header": "지원 센터"
}
}

View File

@@ -212,7 +212,7 @@
"kickParticipantButton": "Verwijderen",
"kickParticipantDialog": "Weet u zeker dat u deze deelnemer wilt verwijderen?",
"kickParticipantTitle": "Deze deelnemer verwijderen?",
"kickTitle": "Oei! {{ParticipantDisplayName}} heeft u uit de vergadering verwijderd",
"kickTitle": "Oei! {{participantDisplayName}} heeft u uit de vergadering verwijderd",
"liveStreaming": "Livestreamen",
"liveStreamingDisabledBecauseOfActiveRecordingTooltip": "Niet mogelijk tijdens opnemen",
"liveStreamingDisabledForGuestTooltip": "Gasten kunnen geen livestream starten.",

View File

@@ -102,6 +102,7 @@
"bandwidth": "Estimated bandwidth:",
"bitrate": "Bitrate:",
"bridgeCount": "Server count: ",
"codecs": "Codecs (A/V): ",
"connectedTo": "Connected to:",
"e2e_rtt": "E2E RTT:",
"framerate": "Frame rate:",
@@ -196,10 +197,7 @@
"displayNameRequired": "Hi! Whats your name?",
"done": "Done",
"e2eeDescription": "End-to-End Encryption is currently EXPERIMENTAL. Please keep in mind that turning on end-to-end encryption will effectively disable server-side provided services such as: recording, live streaming and phone participation. Also keep in mind that the meeting will only work for people joining from browsers with support for insertable streams.",
"e2eeLabel": "E2EE key",
"e2eeNoKey": "None",
"e2eeToggleSet": "Set key",
"e2eeSet": "Set",
"e2eeLabel": "Enable End-to-End Encryption",
"e2eeWarning": "WARNING: Not all participants in this meeting seem to have support for End-to-End encryption. If you enable it they won't be able to see nor hear you.",
"enterDisplayName": "Please enter your name here",
"error": "Error",
@@ -503,6 +501,7 @@
"poweredby": "powered by",
"prejoin": {
"audioAndVideoError": "Audio and video error:",
"audioDeviceProblem": "There is a problem with your audio device",
"audioOnlyError": "Audio error:",
"audioTrackError": "Could not create audio track.",
"calling": "Calling",
@@ -510,6 +509,25 @@
"callMeAtNumber": "Call me at this number:",
"configuringDevices": "Configuring devices...",
"connectedWithAudioQ": "Youre connected with audio?",
"connection": {
"good": "Your internet connection looks good!",
"nonOptimal": "Your internet connection is not optimal",
"poor": "You have a poor internet connection"
},
"connectionDetails": {
"audioClipping": "We expect your audio to be clipped.",
"audioHighQuality": "We expect your audio to have excellent quality.",
"audioLowNoVideo": "We expect your audio quality to be low and no video.",
"goodQuality": "Awesome! Your media quality is going to be great.",
"noMediaConnectivity": "We could not find a way to establish media connectivity for this test. This is typically caused by a firewall or NAT.",
"noVideo": "We expect that your video will be terrible.",
"undetectable": "If you still can not make calls in browser, we recommend that you make sure your speakers, microphone and camera are properly set up, that you have granted your browser rights to use your microphone and camera, and that your browser version is up-to-date. If you still have trouble calling, you should contact the web application developer.",
"veryPoorConnection": "We expect your call quality to be really terrible.",
"videoFreezing": "We expect your video to freeze, turn black, and be pixelated.",
"videoHighQuality": "We expect your video to have good quality.",
"videoLowQuality": "We expect your video to have low quality in terms of frame rate and resolution.",
"videoTearing": "We expect your video to be pixelated or have visual artefacts."
},
"copyAndShare": "Copy & share meeting link",
"dialInMeeting": "Dial into the meeting",
"dialInPin": "Dial into the meeting and enter PIN code:",
@@ -519,6 +537,7 @@
"errorDialOutDisconnected": "Could not dial out. Disconnected",
"errorDialOutFailed": "Could not dial out. Call failed",
"errorDialOutStatus": "Error getting dial out status",
"errorMissingName": "Please enter your name to join the meeting",
"errorStatusCode": "Error dialing out, status code: {{status}}",
"errorValidation": "Number validation failed",
"iWantToDialIn": "I want to dial in",
@@ -675,7 +694,6 @@
"document": "Toggle shared document",
"download": "Download our apps",
"embedMeeting": "Embed meeting",
"e2ee": "End-to-End Encryption",
"feedback": "Leave feedback",
"fullScreen": "Toggle full screen",
"grantModerator": "Grant Moderator",

View File

@@ -15,12 +15,18 @@ import {
} from '../../react/features/base/conference';
import { parseJWTFromURLParams } from '../../react/features/base/jwt';
import { JitsiRecordingConstants } from '../../react/features/base/lib-jitsi-meet';
import { pinParticipant } from '../../react/features/base/participants';
import {
processExternalDeviceRequest
} from '../../react/features/device-selection/functions';
import { isEnabled as isDropboxEnabled } from '../../react/features/dropbox';
import { setE2EEKey } from '../../react/features/e2ee';
import { toggleE2EE } from '../../react/features/e2ee/actions';
import { invite } from '../../react/features/invite';
import {
captureLargeVideoScreenshot,
resizeLargeVideo,
selectParticipantInLargeVideo
} from '../../react/features/large-video/actions';
import { toggleLobbyMode } from '../../react/features/lobby/actions.web';
import { RECORDING_TYPES } from '../../react/features/recording/constants';
import { getActiveSession } from '../../react/features/recording/functions';
@@ -115,14 +121,29 @@ function initCommands() {
));
}
},
'pin-participant': id => {
logger.debug('Pin participant command received');
sendAnalytics(createApiEvent('participant.pinned'));
APP.store.dispatch(pinParticipant(id));
},
'proxy-connection-event': event => {
APP.conference.onProxyConnectionEvent(event);
},
'resize-large-video': (width, height) => {
logger.debug('Resize large video command received');
sendAnalytics(createApiEvent('largevideo.resized'));
APP.store.dispatch(resizeLargeVideo(width, height));
},
'send-tones': (options = {}) => {
const { duration, tones, pause } = options;
APP.store.dispatch(sendTones(tones, duration, pause));
},
'set-large-video-participant': participantId => {
logger.debug('Set large video participant command received');
sendAnalytics(createApiEvent('largevideo.participant.set'));
APP.store.dispatch(selectParticipantInLargeVideo(participantId));
},
'subject': subject => {
sendAnalytics(createApiEvent('subject.changed'));
APP.store.dispatch(setSubject(subject));
@@ -191,9 +212,9 @@ function initCommands() {
logger.error('Failed sending endpoint text message', err);
}
},
'e2ee-key': key => {
logger.debug('Set E2EE key command received');
APP.store.dispatch(setE2EEKey(key));
'toggle-e2ee': enabled => {
logger.debug('Toggle E2EE key command received');
APP.store.dispatch(toggleE2EE(enabled));
},
'set-video-quality': frameHeight => {
logger.debug('Set video quality command received');
@@ -328,6 +349,21 @@ function initCommands() {
const { name } = request;
switch (name) {
case 'capture-largevideo-screenshot' :
APP.store.dispatch(captureLargeVideoScreenshot())
.then(dataURL => {
let error;
if (!dataURL) {
error = new Error('No large video found!');
}
callback({
error,
dataURL
});
});
break;
case 'invite': {
const { invitees } = request;
@@ -524,6 +560,19 @@ class API {
});
}
/**
* Notify external application that the video quality setting has changed.
*
* @param {number} videoQuality - The video quality. The number represents the maximum height of the video streams.
* @returns {void}
*/
notifyVideoQualityChanged(videoQuality: number) {
this._sendEvent({
name: 'video-quality-changed',
videoQuality
});
}
/**
* Notify external application (if API is enabled) that message was
* received.
@@ -677,6 +726,21 @@ class API {
});
}
/**
* Notify external application (if API is enabled) that the an error has been logged.
*
* @param {string} logLevel - The message log level.
* @param {Array} args - Array of strings composing the log message.
* @returns {void}
*/
notifyLog(logLevel: string, args: Array<string>) {
this._sendEvent({
name: 'log',
logLevel,
args
});
}
/**
* Notify external application (if API is enabled) that the conference has
* been joined.
@@ -697,8 +761,7 @@ class API {
}
/**
* Notify external application (if API is enabled) that user changed their
* nickname.
* Notify external application (if API is enabled) that local user has left the conference.
*
* @param {string} roomName - User id.
* @returns {void}

View File

@@ -35,8 +35,11 @@ const commands = {
hangup: 'video-hangup',
muteEveryone: 'mute-everyone',
password: 'password',
pinParticipant: 'pin-participant',
resizeLargeVideo: 'resize-large-video',
sendEndpointTextMessage: 'send-endpoint-text-message',
sendTones: 'send-tones',
setLargeVideoParticipant: 'set-large-video-participant',
setVideoQuality: 'set-video-quality',
startRecording: 'start-recording',
stopRecording: 'stop-recording',
@@ -67,6 +70,7 @@ const events = {
'feedback-prompt-displayed': 'feedbackPromptDisplayed',
'filmstrip-display-changed': 'filmstripDisplayChanged',
'incoming-message': 'incomingMessage',
'log': 'log',
'mic-error': 'micError',
'outgoing-message': 'outgoingMessage',
'participant-joined': 'participantJoined',
@@ -80,6 +84,7 @@ const events = {
'video-conference-left': 'videoConferenceLeft',
'video-availability-changed': 'videoAvailabilityChanged',
'video-mute-status-changed': 'videoMuteStatusChanged',
'video-quality-changed': 'videoQualityChanged',
'screen-sharing-status-changed': 'screenSharingStatusChanged',
'dominant-speaker-changed': 'dominantSpeakerChanged',
'subject-change': 'subjectChange',
@@ -355,6 +360,19 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
);
}
/**
* Returns the formatted display name of a participant.
*
* @param {string} participantId - The id of the participant.
* @returns {string} The formatted display name.
*/
_getFormattedDisplayName(participantId) {
const { formattedDisplayName }
= this._participants[participantId] || {};
return formattedDisplayName;
}
/**
* Returns the id of the on stage participant.
*
@@ -421,10 +439,12 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
const parsedWidth = parseSizeParam(width);
if (parsedHeight !== undefined) {
this._height = height;
this._frame.style.height = parsedHeight;
}
if (parsedWidth !== undefined) {
this._width = width;
this._frame.style.width = parsedWidth;
}
}
@@ -503,6 +523,9 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
changeParticipantNumber(this, -1);
delete this._participants[this._myUserID];
break;
case 'video-quality-changed':
this._videoQuality = data.videoQuality;
break;
}
const eventName = events[name];
@@ -538,6 +561,13 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
* the event and value - the listener.
* Currently we support the following
* events:
* {@code log} - receives event notifications whenever information has
* been logged and has a log level specified within {@code config.apiLogLevels}.
* The listener will receive object with the following structure:
* {{
* logLevel: the message log level
* arguments: an array of strings that compose the actual log message
* }}
* {@code incomingMessage} - receives event notifications about incoming
* messages. The listener will receive object with the following structure:
* {{
@@ -607,6 +637,18 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
}
}
/**
* Captures the screenshot of the large video.
*
* @returns {Promise<string>} - Resolves with a base64 encoded image data of the screenshot
* if large video is detected, an error otherwise.
*/
captureLargeVideoScreenshot() {
return this._transport.sendRequest({
name: 'capture-largevideo-screenshot'
});
}
/**
* Removes the listeners and removes the Jitsi Meet frame.
*
@@ -689,6 +731,32 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
return getCurrentDevices(this._transport);
}
/**
* Returns the conference participants information.
*
* @returns {Array<Object>} - Returns an array containing participants
* information like participant id, display name, avatar URL and email.
*/
getParticipantsInfo() {
const participantIds = Object.keys(this._participants);
const participantsInfo = Object.values(this._participants);
participantsInfo.forEach((participant, idx) => {
participant.participantId = participantIds[idx];
});
return participantsInfo;
}
/**
* Returns the current video quality setting.
*
* @returns {number}
*/
getVideoQuality() {
return this._videoQuality;
}
/**
* Check if the audio is available.
*
@@ -809,19 +877,6 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
return email;
}
/**
* Returns the formatted display name of a participant.
*
* @param {string} participantId - The id of the participant.
* @returns {string} The formatted display name.
*/
_getFormattedDisplayName(participantId) {
const { formattedDisplayName }
= this._participants[participantId] || {};
return formattedDisplayName;
}
/**
* Returns the iframe that loads Jitsi Meet.
*
@@ -865,6 +920,17 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
});
}
/**
* Pins a participant's video on to the stage view.
*
* @param {string} participantId - Participant id (JID) of the participant
* that needs to be pinned on the stage view.
* @returns {void}
*/
pinParticipant(participantId) {
this.executeCommand('pinParticipant', participantId);
}
/**
* Removes event listener.
*
@@ -891,6 +957,19 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
eventList.forEach(event => this.removeEventListener(event));
}
/**
* Resizes the large video container as per the dimensions provided.
*
* @param {number} width - Width that needs to be applied on the large video container.
* @param {number} height - Height that needs to be applied on the large video container.
* @returns {void}
*/
resizeLargeVideo(width, height) {
if (width <= this._width && height <= this._height) {
this.executeCommand('resizeLargeVideo', width, height);
}
}
/**
* Passes an event along to the local conference participant to establish
* or update a direct peer connection. This is currently used for developing
@@ -934,6 +1013,18 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
return setAudioOutputDevice(this._transport, label, deviceId);
}
/**
* Displays the given participant on the large video. If no participant id is specified,
* dominant and pinned speakers will be taken into consideration while selecting the
* the large video participant.
*
* @param {string} participantId - Jid of the participant to be displayed on the large video.
* @returns {void}
*/
setLargeVideoParticipant(participantId) {
this.executeCommand('setLargeVideoParticipant', participantId);
}
/**
* Sets the video input device to the one with the label or id that is
* passed.

View File

@@ -5,13 +5,6 @@
*/
const UIUtil = {
/**
* Returns the available video width.
*/
getAvailableVideoWidth() {
return window.innerWidth;
},
/**
* Escapes the given text.
*/

View File

@@ -15,13 +15,12 @@ import { VIDEO_TYPE } from '../../../react/features/base/media';
import { CHAT_SIZE } from '../../../react/features/chat';
import {
updateKnownLargeVideoResolution
} from '../../../react/features/large-video';
} from '../../../react/features/large-video/actions';
import { PresenceLabel } from '../../../react/features/presence-status';
/* eslint-enable no-unused-vars */
import UIEvents from '../../../service/UI/UIEvents';
import { createDeferred } from '../../util/helpers';
import AudioLevels from '../audio_levels/AudioLevels';
import UIUtil from '../util/UIUtil';
import { VideoContainer, VIDEO_CONTAINER_TYPE } from './VideoContainer';
@@ -68,7 +67,30 @@ export default class LargeVideoManager {
// use the same video container to handle desktop tracks
this.addContainer(DESKTOP_CONTAINER_TYPE, this.videoContainer);
/**
* The preferred width passed as an argument to {@link updateContainerSize}.
*
* @type {number|undefined}
*/
this.preferredWidth = undefined;
/**
* The preferred height passed as an argument to {@link updateContainerSize}.
*
* @type {number|undefined}
*/
this.preferredHeight = undefined;
/**
* The calculated width that will be used for the large video.
* @type {number}
*/
this.width = 0;
/**
* The calculated height that will be used for the large video.
* @type {number}
*/
this.height = 0;
/**
@@ -323,20 +345,32 @@ export default class LargeVideoManager {
/**
* Update container size.
*/
updateContainerSize() {
let widthToUse = UIUtil.getAvailableVideoWidth();
updateContainerSize(width, height) {
if (typeof width === 'number') {
this.preferredWidth = width;
}
if (typeof height === 'number') {
this.preferredHeight = height;
}
let widthToUse = this.preferredWidth || window.innerWidth;
const { isOpen } = APP.store.getState()['features/chat'];
if (isOpen) {
/**
* If chat state is open, we re-compute the container width
* by subtracting the default width of the chat.
*/
/**
* If chat state is open, we re-compute the container width by subtracting the default width of
* the chat. We re-compute the width again after the chat window is closed. This is needed when
* custom styling is configured on the large video container through the iFrame API.
*/
if (isOpen && !this.resizedForChat) {
widthToUse -= CHAT_SIZE;
this.resizedForChat = true;
} else if (this.resizedForChat) {
this.resizedForChat = false;
widthToUse += CHAT_SIZE;
}
this.width = widthToUse;
this.height = window.innerHeight;
this.height = this.preferredHeight || window.innerHeight;
}
/**

View File

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

View File

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

View File

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

View File

@@ -209,7 +209,7 @@ export default {
})
.catch(err => {
audioTrackError = err;
showError && APP.store.disptach(notifyMicError(err));
showError && APP.store.dispatch(notifyMicError(err));
return [];
}));

9121
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -41,7 +41,7 @@
"@tensorflow-models/body-pix": "2.0.4",
"@tensorflow/tfjs": "1.5.1",
"@webcomponents/url": "0.7.1",
"amplitude-js": "4.5.2",
"amplitude-js": "7.1.1",
"base64-js": "1.3.1",
"bc-css-flags": "3.0.0",
"dropbox": "4.0.9",
@@ -56,20 +56,22 @@
"jquery-i18next": "1.2.1",
"js-md5": "0.6.1",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#f74cd0abe9c696a9c3ca7dbb9ca170e6e84d6756",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#a0b8a9c862f46b0e743b8a750c2160acc473ed49",
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
"lodash": "4.17.19",
"moment": "2.19.4",
"moment-duration-format": "2.2.2",
"olm": "https://packages.matrix.org/npm/olm/olm-3.1.4.tgz",
"pixelmatch": "5.1.0",
"punycode": "2.1.1",
"react": "16.9",
"react-dom": "16.9",
"react-emoji-render": "1.2.4",
"react-i18next": "10.11.4",
"react-linkify": "1.0.0-alpha",
"react-native": "github:jitsi/react-native#efd2aff5661d75a230e36406b698cfe0ee545be2",
"react-native-background-timer": "2.1.1",
"react-native-calendar-events": "github:jitsi/react-native-calendar-events#928a80e2ffef0d7e84936d7e7e0acc4f53ee8470",
"react-native-background-timer": "2.4.0",
"react-native-calendar-events": "github:jitsi/react-native-calendar-events#df48ecdc4e1e90c5352f803ddbab1fa7269b74a7",
"react-native-callstats": "3.61.0",
"react-native-collapsible": "1.5.1",
"react-native-default-preference": "1.4.2",
@@ -82,7 +84,7 @@
"react-native-swipeout": "2.3.6",
"react-native-watch-connectivity": "0.4.3",
"react-native-webrtc": "1.84.0",
"react-native-webview": "7.4.1",
"react-native-webview": "10.9.0",
"react-native-youtube-iframe": "1.2.3",
"react-redux": "7.1.0",
"react-textarea-autosize": "7.1.0",
@@ -90,7 +92,8 @@
"redux": "4.0.4",
"redux-thunk": "2.2.0",
"rnnoise-wasm": "github:jitsi/rnnoise-wasm.git#566a16885897704d6e6d67a1d5ac5d39781db2af",
"rtcstats": "github:jitsi/rtcstats#v6.1.3",
"rtcstats": "github:jitsi/rtcstats#v6.2.0",
"stackblur-canvas": "2.3.0",
"styled-components": "3.4.9",
"util": "0.12.1",
"uuid": "3.1.0",
@@ -125,10 +128,9 @@
"expose-loader": "0.7.5",
"flow-bin": "0.104.0",
"imports-loader": "0.7.1",
"jest": "26.1.0",
"jetifier": "1.6.4",
"metro-react-native-babel-preset": "0.56.0",
"node-sass": "4.14.1",
"sass": "1.26.8",
"string-replace-loader": "2.1.1",
"style-loader": "0.19.0",
"unorm": "1.6.0",
@@ -145,7 +147,6 @@
"scripts": {
"lint": "eslint . && flow",
"postinstall": "jetify",
"test": "jest",
"validate": "npm ls"
},
"browser": {

View File

@@ -57,12 +57,15 @@ export function resetAnalytics() {
* @param {Store} store - The redux store in which the specified {@code action} is being dispatched.
* @returns {Promise} Resolves with the handlers that have been successfully loaded.
*/
export function createHandlers({ getState }: { getState: Function }) {
export async function createHandlers({ getState }: { getState: Function }) {
getJitsiMeetGlobalNS().analyticsHandlers = [];
window.analyticsHandlers = []; // Legacy support.
if (!isAnalyticsEnabled(getState)) {
return Promise.resolve([]);
// Avoid all analytics processing if there are no handlers, since no event would be sent.
analytics.dispose();
return [];
}
const state = getState();
@@ -100,43 +103,47 @@ export function createHandlers({ getState }: { getState: Function }) {
};
const handlers = [];
try {
const amplitude = new AmplitudeHandler(handlerConstructorOptions);
if (amplitudeAPPKey) {
try {
const amplitude = new AmplitudeHandler(handlerConstructorOptions);
analytics.amplitudeIdentityProps = amplitude.getIdentityProps();
analytics.amplitudeIdentityProps = amplitude.getIdentityProps();
handlers.push(amplitude);
// eslint-disable-next-line no-empty
} catch (e) {}
handlers.push(amplitude);
} catch (e) {
logger.error('Failed to initialize Amplitude handler', e);
}
}
try {
const matomo = new MatomoHandler(handlerConstructorOptions);
if (matomoEndpoint && matomoSiteID) {
try {
const matomo = new MatomoHandler(handlerConstructorOptions);
handlers.push(matomo);
// eslint-disable-next-line no-empty
} catch (e) {}
handlers.push(matomo);
} catch (e) {
logger.error('Failed to initialize Matomo handler', e);
}
}
return (
_loadHandlers(scriptURLs, handlerConstructorOptions)
.then(externalHandlers => {
handlers.push(...externalHandlers);
if (handlers.length === 0) {
// Throwing an error in order to dispose the analytics in the catch clause due to the lack of any
// analytics handlers.
throw new Error('No analytics handlers created!');
}
if (Array.isArray(scriptURLs) && scriptURLs.length > 0) {
let externalHandlers;
return handlers;
})
.catch(e => {
analytics.dispose();
if (handlers.length !== 0) {
logger.error(e);
}
try {
externalHandlers = await _loadHandlers(scriptURLs, handlerConstructorOptions);
handlers.push(...externalHandlers);
} catch (e) {
logger.error('Failed to initialize external analytics handlers', e);
}
}
return [];
}));
// Avoid all analytics processing if there are no handlers, since no event would be sent.
if (handlers.length === 0) {
analytics.dispose();
}
logger.info(`Initialized ${handlers.length} analytics handlers`);
return handlers;
}
/**
@@ -228,7 +235,7 @@ function _inIframe() {
}
/**
* Tries to load the scripts for the analytics handlers and creates them.
* Tries to load the scripts for the external analytics handlers and creates them.
*
* @param {Array} scriptURLs - The array of script urls to load.
* @param {Object} handlerConstructorOptions - The default options to pass when creating handlers.
@@ -279,7 +286,7 @@ function _loadHandlers(scriptURLs = [], handlerConstructorOptions) {
logger.warn(`Error creating analytics handler: ${error}`);
}
}
logger.debug(`Loaded ${handlers.length} analytics handlers`);
logger.debug(`Loaded ${handlers.length} external analytics handlers`);
return handlers;
});

View File

@@ -72,6 +72,11 @@ export default class AmplitudeHandler extends AbstractHandler {
* @returns {Object}
*/
getIdentityProps() {
// TODO: Remove when web and native Aplitude implementations are unified.
if (navigator.product === 'ReactNative') {
return {};
}
return {
sessionId: amplitude.getInstance(this._amplitudeOptions).getSessionId(),
deviceId: amplitude.getInstance(this._amplitudeOptions).options.deviceId,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -15,8 +15,7 @@ export default [
'abTesting',
'analytics.disabled',
'audioLevelsInterval',
'autoRecord',
'autoRecordToken',
'apiLogLevels',
'avgRtpStatsN',
/**
@@ -69,6 +68,7 @@ export default [
'channelLastN',
'constraints',
'brandingRoomAlias',
'debug',
'debugAudioLevels',
'defaultLanguage',
@@ -100,12 +100,14 @@ export default [
'enableInsecureRoomNameWarning',
'enableLayerSuspension',
'enableLipSync',
'enableOpusRed',
'enableRemb',
'enableScreenshotCapture',
'enableTalkWhileMuted',
'enableNoAudioDetection',
'enableNoisyMicDetection',
'enableTcc',
'enableAutomaticUrlCopy',
'etherpad_base',
'failICE',
'feedbackPercentage',
@@ -115,6 +117,7 @@ export default [
'gatherStats',
'googleApiApplicationClientID',
'hiddenDomain',
'hideLobbyButton',
'hosts',
'iAmRecorder',
'iAmSipGateway',
@@ -148,6 +151,7 @@ export default [
'testing',
'useStunTurn',
'useTurnUdp',
'videoQuality.persist',
'webrtcIceTcpDisable',
'webrtcIceUdpDisable'
].concat(extraConfigWhitelist);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

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

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

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

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,22 @@
// @flow
declare var APP: Object;
/**
* Constructs a log transport object for use with external API.
*
* @param {Array} levels - The log levels forwarded to the external API.
* @returns {Object} - The transport object.
*/
function buildTransport(levels: Array<string>) {
return levels.reduce((logger, level) => {
logger[level] = (...args) => {
APP.API.notifyLog(level, args);
};
return logger;
}, {});
}
export default buildTransport;

View File

@@ -11,6 +11,7 @@ import JitsiMeetJS, {
import { MiddlewareRegistry } from '../redux';
import { isTestModeEnabled } from '../testing';
import buildExternalApiLogTransport from './ExternalApiLogTransport';
import JitsiMeetInMemoryLogStorage from './JitsiMeetInMemoryLogStorage';
import JitsiMeetLogStorage from './JitsiMeetLogStorage';
import { SET_LOGGING_CONFIG } from './actionTypes';
@@ -141,6 +142,15 @@ function _initLogging({ dispatch, getState }, loggingConfig, isTestingEnabled) {
const _logCollector
= new Logger.LogCollector(new JitsiMeetLogStorage(getState));
const { apiLogLevels } = getState()['features/base/config'];
if (apiLogLevels && Array.isArray(apiLogLevels) && typeof APP === 'object') {
const transport = buildExternalApiLogTransport(apiLogLevels);
Logger.addGlobalTransport(transport);
JitsiMeetJS.addGlobalLogTransport(transport);
}
Logger.addGlobalTransport(_logCollector);
JitsiMeetJS.addGlobalLogTransport(_logCollector);
dispatch(setLogCollector(_logCollector));

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,213 @@
// @flow
import { findIndex } from 'lodash';
import { CONNECTION_TYPE } from './constants';
const LOSS_AUDIO_THRESHOLDS = [ 0.33, 0.05 ];
const LOSS_VIDEO_THRESHOLDS = [ 0.33, 0.1, 0.05 ];
const THROUGHPUT_AUDIO_THRESHOLDS = [ 8, 20 ];
const THROUGHPUT_VIDEO_THRESHOLDS = [ 60, 750 ];
/**
* The avatar size to container size ration.
*/
const ratio = 1 / 3;
/**
* The max avatar size.
*/
const maxSize = 190;
/**
* The window limit hight over which the avatar should have the default dimension.
*/
const upperHeightLimit = 760;
/**
* The window limit hight under which the avatar should not be resized anymore.
*/
const lowerHeightLimit = 460;
/**
* The default top margin of the avatar.
*/
const defaultMarginTop = '10%';
/**
* The top margin of the avatar when its dimension is small.
*/
const smallMarginTop = '5%';
/**
* Calculates avatar dimensions based on window height and position.
*
* @param {number} height - The window height.
* @returns {{
* marginTop: string,
* size: number
* }}
*/
export function calculateAvatarDimensions(height: number) {
if (height > upperHeightLimit) {
return {
size: maxSize,
marginTop: defaultMarginTop
};
}
if (height > lowerHeightLimit) {
const diff = height - lowerHeightLimit;
const percent = diff * ratio;
const size = Math.floor(maxSize * percent / 100);
let marginTop = defaultMarginTop;
if (height < 600) {
marginTop = smallMarginTop;
}
return {
size,
marginTop
};
}
return {
size: 0,
marginTop: '0'
};
}
/**
* Returns the level based on a list of thresholds.
*
* @param {number[]} thresholds - The thresholds array.
* @param {number} value - The value against which the level is calculated.
* @param {boolean} descending - The order based on which the level is calculated.
*
* @returns {number}
*/
function _getLevel(thresholds, value, descending = true) {
let predicate;
if (descending) {
predicate = function(threshold) {
return value > threshold;
};
} else {
predicate = function(threshold) {
return value < threshold;
};
}
const i = findIndex(thresholds, predicate);
if (i === -1) {
return thresholds.length;
}
return i;
}
/**
* Returns the connection details from the test results.
*
* @param {{
* fractionalLoss: number,
* throughput: number
* }} testResults - The state of the app.
*
* @returns {{
* connectionType: string,
* connectionDetails: string[]
* }}
*/
function _getConnectionDataFromTestResults({ fractionalLoss: l, throughput: t }) {
const loss = {
audioQuality: _getLevel(LOSS_AUDIO_THRESHOLDS, l),
videoQuality: _getLevel(LOSS_VIDEO_THRESHOLDS, l)
};
const throughput = {
audioQuality: _getLevel(THROUGHPUT_AUDIO_THRESHOLDS, t, false),
videoQuality: _getLevel(THROUGHPUT_VIDEO_THRESHOLDS, t, false)
};
let connectionType = CONNECTION_TYPE.NONE;
const connectionDetails = [];
if (throughput.audioQuality === 0 || loss.audioQuality === 0) {
// Calls are impossible.
connectionType = CONNECTION_TYPE.POOR;
connectionDetails.push('prejoin.connectionDetails.veryPoorConnection');
} else if (
throughput.audioQuality === 2
&& throughput.videoQuality === 2
&& loss.audioQuality === 2
&& loss.videoQuality === 3
) {
// Ideal conditions for both audio and video. Show only one message.
connectionType = CONNECTION_TYPE.GOOD;
connectionDetails.push('prejoin.connectionDetails.goodQuality');
} else {
connectionType = CONNECTION_TYPE.NON_OPTIMAL;
if (throughput.audioQuality === 1) {
// Minimum requirements for a call are met.
connectionDetails.push('prejoin.connectionDetails.audioLowNoVideo');
} else {
// There are two paragraphs: one saying something about audio and the other about video.
if (loss.audioQuality === 1) {
connectionDetails.push('prejoin.connectionDetails.audioClipping');
} else {
connectionDetails.push('prejoin.connectionDetails.audioHighQuality');
}
if (throughput.videoQuality === 0 || loss.videoQuality === 0) {
connectionDetails.push('prejoin.connectionDetails.noVideo');
} else if (throughput.videoQuality === 1) {
connectionDetails.push('prejoin.connectionDetails.videoLowQuality');
} else if (loss.videoQuality === 1) {
connectionDetails.push('prejoin.connectionDetails.videoFreezing');
} else if (loss.videoQuality === 2) {
connectionDetails.push('prejoin.connectionDetails.videoTearing');
} else {
connectionDetails.push('prejoin.connectionDetails.videoHighQuality');
}
}
connectionDetails.push('prejoin.connectionDetails.undetectable');
}
return {
connectionType,
connectionDetails
};
}
/**
* Selector for determining the connection type & details.
*
* @param {Object} state - The state of the app.
* @returns {{
* connectionType: string,
* connectionDetails: string[]
* }}
*/
export function getConnectionData(state: Object) {
const { precallTestResults } = state['features/prejoin'];
if (precallTestResults) {
if (precallTestResults.mediaConnectivity) {
return _getConnectionDataFromTestResults(precallTestResults);
}
return {
connectionType: CONNECTION_TYPE.POOR,
connectionDetails: [ 'prejoin.connectionDetails.noMediaConnectivity' ]
};
}
return {
connectionType: CONNECTION_TYPE.NONE,
connectionDetails: []
};
}

View File

@@ -1,5 +1,6 @@
// @flow
import punycode from 'punycode';
import React, { Component } from 'react';
import ReactLinkify from 'react-linkify';
import { Text } from 'react-native';
@@ -68,7 +69,7 @@ export default class Linkify extends Component<Props> {
key = { key }
style = { this.props.linkStyle }
url = { decoratedHref }>
{decoratedText}
{ punycode.toASCII(decoratedText) }
</Link>
);
}

View File

@@ -1,5 +1,6 @@
// @flow
import punycode from 'punycode';
import React, { Component } from 'react';
import ReactLinkify from 'react-linkify';
@@ -44,7 +45,7 @@ export default class Linkify extends Component<Props> {
key = { key }
rel = 'noopener noreferrer'
target = '_blank'>
{decoratedText}
{ punycode.toASCII(decoratedText) }
</a>
);
}

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