mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-09-15 10:48:59 +00:00
Compare commits
69 Commits
4239
...
release428
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14040650c0 | ||
|
|
0ac7604292 | ||
|
|
fb5dd13c48 | ||
|
|
29805edd02 | ||
|
|
11fd5363ce | ||
|
|
1790c71c80 | ||
|
|
36d95ed51f | ||
|
|
ef0af1a8c0 | ||
|
|
b85cd2348f | ||
|
|
035f720a50 | ||
|
|
c8444a9a0d | ||
|
|
7f5751b918 | ||
|
|
fc6bd3667c | ||
|
|
2c42dd0773 | ||
|
|
1891ce0b24 | ||
|
|
e34c5673b2 | ||
|
|
41ba55a6a9 | ||
|
|
758b60f92b | ||
|
|
fcc69b92bb | ||
|
|
a697caea03 | ||
|
|
5b89709483 | ||
|
|
e4ce3928dc | ||
|
|
b0188a7184 | ||
|
|
53281c2d42 | ||
|
|
3da1b65757 | ||
|
|
0e5091adba | ||
|
|
f376542441 | ||
|
|
bd65108692 | ||
|
|
546b0abe32 | ||
|
|
62ad7d3451 | ||
|
|
5bc3128c71 | ||
|
|
b91d6b97a9 | ||
|
|
ce812591f9 | ||
|
|
f32140c4b7 | ||
|
|
7d513738d2 | ||
|
|
8d1bde3cb1 | ||
|
|
95825dcdd7 | ||
|
|
a61f272303 | ||
|
|
3538761543 | ||
|
|
f22d5ed629 | ||
|
|
f30dd9d881 | ||
|
|
4a3cd2596a | ||
|
|
4e1f42a665 | ||
|
|
1fff5d2567 | ||
|
|
bbcc40a97e | ||
|
|
bbf76296ed | ||
|
|
e0b3a81a41 | ||
|
|
1c122705bf | ||
|
|
29c16e42bd | ||
|
|
8a19a34d19 | ||
|
|
d5832f226d | ||
|
|
4cfc8cd7a2 | ||
|
|
873ede0e06 | ||
|
|
f73e9947c0 | ||
|
|
82711b3f23 | ||
|
|
2f841fab73 | ||
|
|
b3a2905849 | ||
|
|
5f579e9a15 | ||
|
|
ea2ea89ef7 | ||
|
|
a5f17a8033 | ||
|
|
4c6e9e7788 | ||
|
|
a7e0df2623 | ||
|
|
da9a70129e | ||
|
|
6d3d15a64b | ||
|
|
b10a45bf98 | ||
|
|
858a3d953c | ||
|
|
1ff27b7298 | ||
|
|
bc43f00d28 | ||
|
|
bfd5db355d |
2
Makefile
2
Makefile
@@ -74,7 +74,7 @@ deploy-rnnoise-binary:
|
||||
|
||||
deploy-css:
|
||||
$(NODE_SASS) $(STYLES_MAIN) $(STYLES_BUNDLE) && \
|
||||
$(CLEANCSS) $(STYLES_BUNDLE) > $(STYLES_DESTINATION) ; \
|
||||
$(CLEANCSS) --skip-rebase $(STYLES_BUNDLE) > $(STYLES_DESTINATION) ; \
|
||||
rm $(STYLES_BUNDLE)
|
||||
|
||||
deploy-local:
|
||||
|
||||
@@ -147,6 +147,7 @@ public class MainActivity extends JitsiMeetActivity {
|
||||
.setWelcomePageEnabled(true)
|
||||
.setServerURL(buildURL(defaultURL))
|
||||
.setFeatureFlag("call-integration.enabled", false)
|
||||
.setFeatureFlag("resolution", 360)
|
||||
.setFeatureFlag("server-url-change.enabled", !configurationByRestrictions)
|
||||
.build();
|
||||
JitsiMeet.setDefaultConferenceOptions(defaultOptions);
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
<network-security-config>
|
||||
<domain-config cleartextTrafficPermitted="true">
|
||||
<domain includeSubdomains="false">localhost</domain>
|
||||
<domain includeSubdomains="false">10.0.2.2</domain>
|
||||
</domain-config>
|
||||
<network-security-config>
|
||||
<base-config>
|
||||
<trust-anchors>
|
||||
<certificates src="system" />
|
||||
<certificates src="user" />
|
||||
</trust-anchors>
|
||||
</base-config>
|
||||
<domain-config cleartextTrafficPermitted="true">
|
||||
<domain includeSubdomains="false">localhost</domain>
|
||||
<domain includeSubdomains="false">10.0.2.2</domain>
|
||||
</domain-config>
|
||||
</network-security-config>
|
||||
|
||||
@@ -411,6 +411,10 @@ function disconnect() {
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
if (!connection) {
|
||||
return onDisconnected();
|
||||
}
|
||||
|
||||
return connection.disconnect().then(onDisconnected, onDisconnected);
|
||||
}
|
||||
|
||||
|
||||
24
config.js
24
config.js
@@ -44,6 +44,10 @@ var config = {
|
||||
//
|
||||
|
||||
testing: {
|
||||
// Disables the End to End Encryption feature. Useful for debugging
|
||||
// issues related to insertable streams.
|
||||
// disableE2EE: false,
|
||||
|
||||
// P2P test mode disables automatic switching to P2P when there are 2
|
||||
// participants in the conference.
|
||||
p2pTestMode: false
|
||||
@@ -107,6 +111,11 @@ var config = {
|
||||
// participants and to enable it back a reload is needed.
|
||||
// startSilent: false
|
||||
|
||||
// Sets the preferred target bitrate for the Opus audio codec by setting its
|
||||
// 'maxaveragebitrate' parameter. Currently not available in p2p mode.
|
||||
// Valid values are in the range 6000 to 510000
|
||||
// opusMaxAvgBitrate: 20000,
|
||||
|
||||
// Video
|
||||
|
||||
// Sets the preferred resolution (height) for local video. Defaults to 720.
|
||||
@@ -306,7 +315,7 @@ var config = {
|
||||
// When 'true', it shows an intermediate page before joining, where the user can configure their devices.
|
||||
// prejoinPageEnabled: false,
|
||||
|
||||
// If true, shows the unsafe roon name warning label when a room name is
|
||||
// If true, shows the unsafe room name warning label when a room name is
|
||||
// deemed unsafe (due to the simplicity in the name) and a password is not
|
||||
// set or the lobby is not enabled.
|
||||
// enableInsecureRoomNameWarning: false,
|
||||
@@ -361,7 +370,7 @@ var config = {
|
||||
// The STUN servers that will be used in the peer to peer connections
|
||||
stunServers: [
|
||||
|
||||
// { urls: 'stun:jitsi-meet.example.com:4446' },
|
||||
// { urls: 'stun:jitsi-meet.example.com:3478' },
|
||||
{ urls: 'stun:meet-jit-si-turnrelay.jitsi.net:443' }
|
||||
]
|
||||
|
||||
@@ -397,6 +406,15 @@ var config = {
|
||||
// The Amplitude APP Key:
|
||||
// amplitudeAPPKey: '<APP_KEY>'
|
||||
|
||||
// Configuration for the rtcstats server:
|
||||
// In order to enable rtcstats one needs to provide a endpoint url.
|
||||
// rtcstatsEndpoint: wss://rtcstats-server-pilot.jitsi.net/,
|
||||
|
||||
// The interval at which rtcstats will poll getStats, defaults to 1000ms.
|
||||
// If the value is set to 0 getStats won't be polled and the rtcstats client
|
||||
// will only send data related to RTCPeerConnection events.
|
||||
// rtcstatsPolIInterval: 1000
|
||||
|
||||
// Array of script URLs to load as lib-jitsi-meet "analytics handlers".
|
||||
// scriptURLs: [
|
||||
// "libs/analytics-ga.min.js", // google-analytics
|
||||
@@ -504,7 +522,7 @@ var config = {
|
||||
/**
|
||||
External API url used to receive branding specific information.
|
||||
If there is no url set or there are missing fields, the defaults are applied.
|
||||
None of the fieds are mandatory and the response must have the shape:
|
||||
None of the fields are mandatory and the response must have the shape:
|
||||
{
|
||||
// The hex value for the colour used as background
|
||||
backgroundColor: '#fff',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* global APP, JitsiMeetJS, config */
|
||||
|
||||
import { jitsiLocalStorage } from '@jitsi/js-utils';
|
||||
import Logger from 'jitsi-meet-logger';
|
||||
import { jitsiLocalStorage } from 'js-utils';
|
||||
|
||||
import AuthHandler from './modules/UI/authentication/AuthHandler';
|
||||
import {
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
JitsiConnectionErrors,
|
||||
JitsiConnectionEvents
|
||||
} from './react/features/base/lib-jitsi-meet';
|
||||
import { setPrejoinDisplayNameRequired } from './react/features/prejoin/actions';
|
||||
|
||||
const logger = Logger.getLogger(__filename);
|
||||
|
||||
@@ -113,6 +114,10 @@ function connect(id, password, roomName) {
|
||||
connection.addEventListener(
|
||||
JitsiConnectionEvents.CONNECTION_FAILED,
|
||||
connectionFailedHandler);
|
||||
connection.addEventListener(
|
||||
JitsiConnectionEvents.DISPLAY_NAME_REQUIRED,
|
||||
displayNameRequiredHandler
|
||||
);
|
||||
|
||||
/* eslint-disable max-params */
|
||||
/**
|
||||
@@ -166,6 +171,14 @@ function connect(id, password, roomName) {
|
||||
reject(err);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the display name for the prejoin screen as required.
|
||||
* This can happen if a user tries to join a room with lobby enabled.
|
||||
*/
|
||||
function displayNameRequiredHandler() {
|
||||
APP.store.dispatch(setPrejoinDisplayNameRequired());
|
||||
}
|
||||
|
||||
checkForAttachParametersAndConnect(id, password, connection);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -96,6 +96,11 @@
|
||||
padding: 0 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.prejoin-dialog-btn.primary,
|
||||
.action-btn.prejoin-dialog-btn.text {
|
||||
width: 310px;
|
||||
}
|
||||
}
|
||||
|
||||
.prejoin-dialog-callout {
|
||||
|
||||
@@ -36,13 +36,7 @@
|
||||
}
|
||||
|
||||
&-checkbox-container {
|
||||
align-items: center;
|
||||
color: #fff;
|
||||
display: none;
|
||||
font-size: 13px;
|
||||
justify-content: center;
|
||||
line-height: 20px;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 14px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,90 @@
|
||||
/**
|
||||
* Shared style for full screen local track based dialogs/modals.
|
||||
*/
|
||||
.premeeting-screen,
|
||||
.preview-overlay {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.premeeting-screen {
|
||||
align-items: stretch;
|
||||
background: #1C2025;
|
||||
bottom: 0;
|
||||
background: radial-gradient(50% 50% at 50% 50%, #5D95C7 0%, #376288 100%), #FFFFFF;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 1.3em;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
z-index: $toolbarZ + 1;
|
||||
|
||||
.action-btn {
|
||||
border-radius: 3px;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-size: 15px;
|
||||
line-height: 24px;
|
||||
margin-top: 16px;
|
||||
padding: 7px 16px;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
width: 286px;
|
||||
|
||||
&.primary {
|
||||
background: #0376DA;
|
||||
border: 1px solid #0376DA;
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
background: transparent;
|
||||
border: 1px solid #5E6D7A;
|
||||
}
|
||||
|
||||
&.text {
|
||||
width: auto;
|
||||
font-size: 13px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
background: #5E6D7A;
|
||||
border: 1px solid #5E6D7A;
|
||||
color: #AFB6BC;
|
||||
cursor: initial;
|
||||
|
||||
.icon {
|
||||
& > svg {
|
||||
fill: #AFB6BC;
|
||||
}
|
||||
}
|
||||
|
||||
.options {
|
||||
border-left: 1px solid #AFB6BC;
|
||||
}
|
||||
}
|
||||
|
||||
.options {
|
||||
align-items: center;
|
||||
border-left: 1px solid #fff;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-overlay {
|
||||
background-image: linear-gradient(transparent, black);
|
||||
z-index: $toolbarZ + 1;
|
||||
}
|
||||
|
||||
.content {
|
||||
align-items: center;
|
||||
background-image: linear-gradient(transparent, black);
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
@@ -97,66 +165,6 @@
|
||||
color: $defaultWarningColor;
|
||||
}
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
border-radius: 3px;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-size: 15px;
|
||||
line-height: 24px;
|
||||
margin-top: 16px;
|
||||
padding: 7px 16px;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
width: 286px;
|
||||
|
||||
&.primary {
|
||||
background: #0376DA;
|
||||
border: 1px solid #0376DA;
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
background: transparent;
|
||||
border: 1px solid #5E6D7A;
|
||||
}
|
||||
|
||||
&.text {
|
||||
width: auto;
|
||||
font-size: 13px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
background: #5E6D7A;
|
||||
border: 1px solid #5E6D7A;
|
||||
color: #AFB6BC;
|
||||
cursor: initial;
|
||||
|
||||
.icon {
|
||||
& > svg {
|
||||
fill: #AFB6BC;
|
||||
}
|
||||
}
|
||||
|
||||
.options {
|
||||
border-left: 1px solid #AFB6BC;
|
||||
}
|
||||
}
|
||||
|
||||
.options {
|
||||
align-items: center;
|
||||
border-left: 1px solid #fff;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: 40px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.media-btn-container {
|
||||
@@ -189,9 +197,16 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.preview-avatar-container {
|
||||
width: 100%;
|
||||
height: 80%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
background: #A4B8D1;
|
||||
margin: 200px auto 0 auto;
|
||||
}
|
||||
|
||||
video {
|
||||
@@ -201,3 +216,66 @@
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin flex-centered() {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@mixin icon-container($bg, $fill) {
|
||||
.toggle-button-icon-container {
|
||||
background: $bg;
|
||||
|
||||
svg {
|
||||
fill: $fill
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-button {
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
height: 40px;
|
||||
margin: 0 auto;
|
||||
width: 320px;
|
||||
|
||||
@include flex-centered();
|
||||
|
||||
svg {
|
||||
fill: transparent;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #1C2025;
|
||||
|
||||
@include icon-container(#A4B8D1, #1C2025);
|
||||
}
|
||||
|
||||
&-container {
|
||||
position: relative;
|
||||
|
||||
@include flex-centered();
|
||||
}
|
||||
|
||||
&-icon-container {
|
||||
border-radius: 50%;
|
||||
left: -22px;
|
||||
padding: 2px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
&--toggled {
|
||||
background: #75757A;
|
||||
|
||||
&:hover {
|
||||
background: #75757A;
|
||||
|
||||
@include icon-container(#A4B8D1, #75757A);
|
||||
}
|
||||
|
||||
@include icon-container(#A4B8D1, #75757A);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,6 +164,9 @@ $unsupportedDesktopBrowserTextFontSize: 21px;
|
||||
$watermarkWidth: 186px;
|
||||
$watermarkHeight: 74px;
|
||||
|
||||
$welcomePageWatermarkWidth: 186px;
|
||||
$welcomePageWatermarkHeight: 74px;
|
||||
|
||||
/**
|
||||
* Welcome page variables.
|
||||
*/
|
||||
@@ -178,9 +181,12 @@ $welcomePageHeaderBackgroundPosition: none;
|
||||
$welcomePageHeaderBackgroundRepeat: none;
|
||||
$welcomePageHeaderBackgroundSize: none;
|
||||
$welcomePageHeaderPaddingBottom: 0px;
|
||||
$welcomePageHeaderMinHeight: fit-content;
|
||||
|
||||
$welcomePageHeaderTextMarginTop: 35px;
|
||||
$welcomePageHeaderTextMarginBottom: 35px;
|
||||
$welcomePageHeaderTextDisplay: flex;
|
||||
$welcomePageHeaderTextWidth: 650px;
|
||||
|
||||
$welcomePageHeaderTextTitleMarginBottom: 16px;
|
||||
$welcomePageHeaderTextTitleFontSize: 2.5rem;
|
||||
@@ -195,6 +201,7 @@ $welcomePageHeaderTextDescriptionLineHeight: 24px;
|
||||
$welcomePageHeaderTextDescriptionMarginBottom: 20px;
|
||||
$welcomePageHeaderTextDescriptionAlignSelf: inherit;
|
||||
|
||||
$welcomePageEnterRoomDisplay: flex;
|
||||
$welcomePageEnterRoomWidth: 680px;
|
||||
$welcomePageEnterRoomPadding: 25px 30px;
|
||||
$welcomePageEnterRoomBorderRadius: 0px;
|
||||
|
||||
@@ -21,18 +21,18 @@ body.welcome-page {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: fit-content;
|
||||
min-height: $welcomePageHeaderMinHeight;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
|
||||
.header-text {
|
||||
display: flex;
|
||||
display: $welcomePageHeaderTextDisplay;
|
||||
flex-direction: column;
|
||||
margin-top: $watermarkHeight + $welcomePageHeaderTextMarginTop;
|
||||
margin-bottom: $welcomePageHeaderTextMarginBottom;
|
||||
max-width: calc(100% - 40px);
|
||||
width: 650px;
|
||||
width: $welcomePageHeaderTextWidth;
|
||||
z-index: $zindex2;
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ body.welcome-page {
|
||||
}
|
||||
|
||||
#enter_room {
|
||||
display: flex;
|
||||
display: $welcomePageEnterRoomDisplay;
|
||||
align-items: center;
|
||||
max-width: calc(100% - 40px);
|
||||
width: $welcomePageEnterRoomWidth;
|
||||
@@ -211,5 +211,10 @@ body.welcome-page {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.watermark.leftwatermark {
|
||||
width: $welcomePageWatermarkWidth;
|
||||
height: $welcomePageWatermarkHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,10 @@
|
||||
font-size: 14px;
|
||||
color: #6FB1EA;
|
||||
}
|
||||
|
||||
& > :first-child:not(:last-child) {
|
||||
margin-right: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
33
debian/jitsi-meet-turnserver.postinst
vendored
33
debian/jitsi-meet-turnserver.postinst
vendored
@@ -49,7 +49,7 @@ case "$1" in
|
||||
# nothing to do
|
||||
echo "------------------------------------------------"
|
||||
echo ""
|
||||
echo "turnserver is listening on tcp 4445 as other nginx sites use port 443"
|
||||
echo "turnserver is listening on tcp 5349 as other nginx sites use port 443"
|
||||
echo ""
|
||||
echo "------------------------------------------------"
|
||||
NGINX_MULTIPLEXING="false"
|
||||
@@ -87,9 +87,36 @@ case "$1" in
|
||||
if [[ -f $TURN_CONFIG ]] ; then
|
||||
echo "------------------------------------------------"
|
||||
echo ""
|
||||
echo "turnserver is already configured on this machine, skipping."
|
||||
echo "turnserver is already configured on this machine."
|
||||
echo ""
|
||||
echo "------------------------------------------------"
|
||||
|
||||
if grep -q "jitsi-meet coturn config" "$TURN_CONFIG" && ! grep -q "jitsi-meet coturn relay disable config" "$TURN_CONFIG" ; then
|
||||
echo "Updating coturn config"
|
||||
echo "# jitsi-meet coturn relay disable config. Do not modify this line
|
||||
no-multicast-peers
|
||||
no-cli
|
||||
no-loopback-peers
|
||||
no-tcp-relay
|
||||
denied-peer-ip=0.0.0.0-0.255.255.255
|
||||
denied-peer-ip=10.0.0.0-10.255.255.255
|
||||
denied-peer-ip=100.64.0.0-100.127.255.255
|
||||
denied-peer-ip=127.0.0.0-127.255.255.255
|
||||
denied-peer-ip=169.254.0.0-169.254.255.255
|
||||
denied-peer-ip=127.0.0.0-127.255.255.255
|
||||
denied-peer-ip=172.16.0.0-172.31.255.255
|
||||
denied-peer-ip=192.0.0.0-192.0.0.255
|
||||
denied-peer-ip=192.0.2.0-192.0.2.255
|
||||
denied-peer-ip=192.88.99.0-192.88.99.255
|
||||
denied-peer-ip=192.168.0.0-192.168.255.255
|
||||
denied-peer-ip=198.18.0.0-198.19.255.255
|
||||
denied-peer-ip=198.51.100.0-198.51.100.255
|
||||
denied-peer-ip=203.0.113.0-203.0.113.255
|
||||
denied-peer-ip=240.0.0.0-255.255.255.255" >> $TURN_CONFIG
|
||||
|
||||
invoke-rc.d coturn restart || true
|
||||
fi
|
||||
|
||||
db_stop
|
||||
exit 0
|
||||
fi
|
||||
@@ -152,7 +179,7 @@ case "$1" in
|
||||
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"/"4445"/g' $PROSODY_HOST_CONFIG
|
||||
sed -i 's/"443"/"5349"/g' $PROSODY_HOST_CONFIG
|
||||
invoke-rc.d prosody restart || true
|
||||
fi
|
||||
fi
|
||||
|
||||
2
debian/jitsi-meet-web-config.postinst
vendored
2
debian/jitsi-meet-web-config.postinst
vendored
@@ -98,7 +98,7 @@ case "$1" in
|
||||
-reqexts SAN \
|
||||
-extensions SAN \
|
||||
-config <(cat /etc/ssl/openssl.cnf \
|
||||
<(printf '[SAN]\nsubjectAltName=DNS:localhost,DNS:$JVB_HOSTNAME,IP:$JVB_HOSTNAME'))
|
||||
<(printf "[SAN]\nsubjectAltName=DNS:localhost,DNS:$JVB_HOSTNAME"))
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ muc_mapper_domain_base = "jitmeet.example.com";
|
||||
turncredentials_secret = "__turnSecret__";
|
||||
|
||||
turncredentials = {
|
||||
{ type = "stun", host = "jitmeet.example.com", port = "4446" },
|
||||
{ type = "turn", host = "jitmeet.example.com", port = "4446", transport = "udp" },
|
||||
{ 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" }
|
||||
};
|
||||
|
||||
|
||||
@@ -5,14 +5,32 @@ static-auth-secret=__turnSecret__
|
||||
realm=jitsi-meet.example.com
|
||||
cert=/etc/jitsi/meet/jitsi-meet.example.com.crt
|
||||
pkey=/etc/jitsi/meet/jitsi-meet.example.com.key
|
||||
|
||||
no-multicast-peers
|
||||
no-cli
|
||||
no-loopback-peers
|
||||
no-tcp-relay
|
||||
no-tcp
|
||||
listening-port=4446
|
||||
tls-listening-port=4445
|
||||
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
|
||||
cipher-list=ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
|
||||
|
||||
# jitsi-meet coturn relay disable config. Do not modify this line
|
||||
denied-peer-ip=0.0.0.0-0.255.255.255
|
||||
denied-peer-ip=10.0.0.0-10.255.255.255
|
||||
denied-peer-ip=100.64.0.0-100.127.255.255
|
||||
denied-peer-ip=127.0.0.0-127.255.255.255
|
||||
denied-peer-ip=169.254.0.0-169.254.255.255
|
||||
denied-peer-ip=127.0.0.0-127.255.255.255
|
||||
denied-peer-ip=172.16.0.0-172.31.255.255
|
||||
denied-peer-ip=192.0.0.0-192.0.0.255
|
||||
denied-peer-ip=192.0.2.0-192.0.2.255
|
||||
denied-peer-ip=192.88.99.0-192.88.99.255
|
||||
denied-peer-ip=192.168.0.0-192.168.255.255
|
||||
denied-peer-ip=198.18.0.0-198.19.255.255
|
||||
denied-peer-ip=198.51.100.0-198.51.100.255
|
||||
denied-peer-ip=203.0.113.0-203.0.113.255
|
||||
denied-peer-ip=240.0.0.0-255.255.255.255
|
||||
syslog
|
||||
|
||||
@@ -7,7 +7,7 @@ stream {
|
||||
server 127.0.0.1:4444;
|
||||
}
|
||||
upstream turn {
|
||||
server 127.0.0.1:4445;
|
||||
server 127.0.0.1:5349;
|
||||
}
|
||||
# since 1.13.10
|
||||
map $ssl_preread_alpn_protocols $upstream {
|
||||
|
||||
10
index.html
10
index.html
@@ -8,7 +8,17 @@
|
||||
|
||||
<link rel="apple-touch-icon" href="images/apple-touch-icon.png">
|
||||
<link rel="stylesheet" href="css/all.css">
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (!JitsiMeetJS.app) {
|
||||
return;
|
||||
}
|
||||
|
||||
JitsiMeetJS.app.renderEntryPoint({
|
||||
Component: JitsiMeetJS.app.entryPoints.APP
|
||||
})
|
||||
})
|
||||
</script>
|
||||
<script>
|
||||
// IE11 and earlier can be identified via their user agent and be
|
||||
// redirected to a page that is known to have no newer js syntax.
|
||||
|
||||
@@ -48,6 +48,7 @@ var interfaceConfig = {
|
||||
DEFAULT_LOCAL_DISPLAY_NAME: 'me',
|
||||
DEFAULT_LOGO_URL: 'images/watermark.png',
|
||||
DEFAULT_REMOTE_DISPLAY_NAME: 'Fellow Jitster',
|
||||
DEFAULT_WELCOME_PAGE_LOGO_URL: 'images/watermark.png',
|
||||
|
||||
DISABLE_DOMINANT_SPEAKER_INDICATOR: false,
|
||||
|
||||
|
||||
@@ -293,7 +293,7 @@ PODS:
|
||||
- React
|
||||
- react-native-netinfo (4.1.5):
|
||||
- React
|
||||
- react-native-webrtc (1.75.3):
|
||||
- react-native-webrtc (1.84.0):
|
||||
- React
|
||||
- react-native-webview (7.4.1):
|
||||
- React
|
||||
@@ -569,7 +569,7 @@ SPEC CHECKSUMS:
|
||||
react-native-calendar-events: 1442fad71a00388f933cfa25512588fec300fcf8
|
||||
react-native-keep-awake: eba3137546b10003361b37c761f6c429b59814ae
|
||||
react-native-netinfo: 8d8db463bcc5db66a8ac5c48a7d86beb3b92f61a
|
||||
react-native-webrtc: 86d841823e66d68cc1f86712db1c2956056bf0c2
|
||||
react-native-webrtc: 9268ae9a2bc9730796b0968d012327e92c392adf
|
||||
react-native-webview: 4dbc1d2a4a6b9c5e9e723c62651917aa2b5e579e
|
||||
React-RCTActionSheet: b72ddbfbe15b44ce691d128e4b582f4bb9abb540
|
||||
React-RCTAnimation: cfaefba5024499d336b76ab850e6bd33b232b5e3
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
jitsiMeet.universalLinkDomains = @[@"meet.jit.si", @"alpha.jitsi.net", @"beta.meet.jit.si"];
|
||||
|
||||
jitsiMeet.defaultConferenceOptions = [JitsiMeetConferenceOptions fromBuilder:^(JitsiMeetConferenceOptionsBuilder *builder) {
|
||||
[builder setFeatureFlag:@"resolution" withValue:@(360)];
|
||||
builder.serverURL = [NSURL URLWithString:@"https://meet.jit.si"];
|
||||
builder.welcomePageEnabled = YES;
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,21 +1,36 @@
|
||||
{
|
||||
"addPeople": {
|
||||
"add": "Inviter",
|
||||
"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",
|
||||
"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",
|
||||
"disabled": "Vous ne pouvez pas inviter quelqu'un.",
|
||||
"failedToAdd": "Erreur lors de l'ajout des participants",
|
||||
"footerText": "Appels sortants désactivés.",
|
||||
"googleEmail": "Gmail",
|
||||
"inviteMoreHeader": "Vous êtes seul(e) dans la réunion",
|
||||
"inviteMoreMailSubject": "Rejoindre une réunion {{appName}}",
|
||||
"inviteMorePrompt": "Inviter d'autres personnes",
|
||||
"linkCopied": "Lien copié dans le presse-papiers",
|
||||
"loading": "Rechercher des personnes et des numéros de téléphone",
|
||||
"loadingNumber": "Validation du numéro de téléphone",
|
||||
"loadingPeople": "Recherche de personnes à inviter",
|
||||
"noResults": "Aucun résultat de recherche correspondant",
|
||||
"noValidNumbers": "Veuillez entrer un numéro de téléphone",
|
||||
"outlookEmail": "Outlook",
|
||||
"searchNumbers": "Ajouter des numéros de téléphone",
|
||||
"searchPeople": "Rechercher une personne",
|
||||
"searchPeopleAndNumbers": "Rechercher des personnes ou ajouter leurs numéros de téléphone",
|
||||
"shareInvite": "Partager l'invitation à la réunion",
|
||||
"shareLink": "Partager le lien de la réunion pour inviter d'autres personnes",
|
||||
"shareStream": "Partager le lien de diffusion en direct",
|
||||
"telephone": "Téléphone : {{number}}",
|
||||
"title": "Inviter une personne à cette réunion"
|
||||
"title": "Inviter une personne à cette réunion",
|
||||
"yahooEmail": "Yahoo"
|
||||
},
|
||||
"audioDevices": {
|
||||
"bluetooth": "Bluetooth",
|
||||
@@ -147,6 +162,7 @@
|
||||
"accessibilityLabel": {
|
||||
"liveStreaming": "Diffusion en direct"
|
||||
},
|
||||
"add": "Ajouter",
|
||||
"allow": "Autoriser",
|
||||
"alreadySharedVideoMsg": "Un autre participant est en train de partager sa vidéo. Cette conférence ne permet de partager qu'une seule vidéo à la fois.",
|
||||
"alreadySharedVideoTitle": "Une seule vidéo partagée est autorisée à la fois",
|
||||
@@ -468,6 +484,40 @@
|
||||
"passwordSetRemotely": "défini par un autre participant",
|
||||
"passwordDigitsOnly": "Jusqu'à {{number}} chiffres",
|
||||
"poweredby": "produit par",
|
||||
"prejoin": {
|
||||
"audioAndVideoError": "Erreur audio et video:",
|
||||
"audioOnlyError": "Erreur audio:",
|
||||
"audioTrackError": "N'a pas pu créer la piste audio.",
|
||||
"callMe": "Appelez-moi",
|
||||
"callMeAtNumber": "Appelez-moi à ce numéro:",
|
||||
"configuringDevices": "Configuration des appareils...",
|
||||
"connectedWithAudioQ": "Êtes-vous connecté avec le microphone?",
|
||||
"copyAndShare": "Copier & partager le lien",
|
||||
"dialInMeeting": "Participez à la réunion",
|
||||
"dialInPin": "Participez à la réunion et saisir le code PIN:",
|
||||
"dialing": "Numérotation",
|
||||
"doNotShow": "Ne plus afficher ceci",
|
||||
"errorDialOut": "Impossible de composer le numéro",
|
||||
"errorDialOutDisconnected": "Impossible de composer le numéro. Déconnecté",
|
||||
"errorDialOutFailed": "Impossible de composer le numéro. L'appel a échoué",
|
||||
"errorDialOutStatus": "Erreur lors de l'obtention de l'état d'appel sortant",
|
||||
"errorStatusCode": "Erreur de numérotation, code d'état: {{status}}",
|
||||
"errorValidation": "La validation du numéro a échoué",
|
||||
"iWantToDialIn": "Je veux me connecter",
|
||||
"joinAudioByPhone": "Rejoindre avec l'audio du téléphone",
|
||||
"joinMeeting": "Rejoindre la réunion",
|
||||
"joinWithoutAudio": "Rejoignez sans microphone",
|
||||
"initiated": "Appel lancé",
|
||||
"linkCopied": "Lien copié dans le presse-papiers",
|
||||
"lookGood": "Il semble que votre microphone fonctionne correctement",
|
||||
"or": "ou",
|
||||
"calling": "Appel",
|
||||
"startWithPhone": "Commencez avec l'audio du téléphone",
|
||||
"screenSharingError": "Erreur de partage d'écran:",
|
||||
"videoOnlyError": "Erreur vidéo:",
|
||||
"videoTrackError": "Impossible de créer une piste vidéo.",
|
||||
"viewAllNumbers": "voir tous les numéros"
|
||||
},
|
||||
"presenceStatus": {
|
||||
"busy": "Occupé",
|
||||
"calling": "Appel...",
|
||||
@@ -783,5 +833,39 @@
|
||||
},
|
||||
"helpView": {
|
||||
"header": "Centre d'aide"
|
||||
},
|
||||
"lobby": {
|
||||
"knockingParticipantList" : "Liste des participants en attente",
|
||||
"allow": "Autoriser",
|
||||
"backToKnockModeButton": "Aucun mot de passe, demander à rejoindre plutôt",
|
||||
"dialogTitle": "Mode lobby",
|
||||
"disableDialogContent": "Le mode lobby est actuellement activé. Cette fonctionnalité garantit que les participants indésirables ne peuvent pas rejoindre votre réunion. Souhaitez-vous la désactiver?",
|
||||
"disableDialogSubmit": "Désactiver",
|
||||
"emailField": "Saisissez votre adresse email",
|
||||
"enableDialogPasswordField": "Définir le mot de passe (optionel)",
|
||||
"enableDialogSubmit": "Activer",
|
||||
"enableDialogText": "Le mode lobby vous permet de protéger votre réunion en autorisant les personnes à entrer qu'après l'approbation formelle d'un modérateur.",
|
||||
"enterPasswordButton": "Saisissez un mot de passe de réunion",
|
||||
"enterPasswordTitle": "Saisissez le mot de passe pour rejoindre la réunion",
|
||||
"invalidPassword": "Mot de passe invalide",
|
||||
"joiningMessage": "Vous allez rejoindre une réunion dès que quelqu'un aura accepté votre demande",
|
||||
"joinWithPasswordMessage": "Tentative de rejoindre avec mot de passe, patientez s'il vous plait...",
|
||||
"joinRejectedMessage": "Votre requête pour rejoindre une réunion a été refusée par un modérateur.",
|
||||
"joinTitle": "Rejoindre une réunion",
|
||||
"joiningTitle": "Demander à rejoindre une réunion...",
|
||||
"joiningWithPasswordTitle": "Rejoindre avec mot de passe...",
|
||||
"knockButton": "Demander à rejoindre",
|
||||
"knockTitle": "Quelqu'un souhaite rejoindre la réunion",
|
||||
"nameField": "Saisissez votre nom",
|
||||
"passwordField": "Saisissez le mot de passe de la réunion",
|
||||
"passwordJoinButton": "Rejoindre",
|
||||
"reject": "Refuser",
|
||||
"toggleLabel": "Activer le lobby"
|
||||
},
|
||||
"security": {
|
||||
"about": "Vous pouvez ajouter un mot de passe à votre réunion. Les participants devront fournir le mot de passe avant qu'ils soient autorisés à rejoindre la réunion.",
|
||||
"aboutReadOnly": "Les modérateurs peuvent ajouter un mot de passe à la réunion. Les participants devront fournir le mot de passe avant qu'ils soient autorisés à rejoindre la réunion.",
|
||||
"insecureRoomNameWarning": "Le nom de la salle est peu sûr. Des participants non désirés peuvent rejoindre votre réunion. Pensez à sécuriser votre réunion en cliquant sur le bouton de sécurité.",
|
||||
"securityOptions": "Options de sécurité"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,9 +290,9 @@
|
||||
"inviteLiveStream": "この会議のライブストリームを表示するには、このリンクをクリックしてください:{{url}}",
|
||||
"invitePhone": "",
|
||||
"invitePhoneAlternatives": "",
|
||||
"inviteURLFirstPartGeneral": "",
|
||||
"inviteURLFirstPartPersonal": "",
|
||||
"inviteURLSecondPart": "",
|
||||
"inviteURLFirstPartGeneral": "あなたはミーティングに招待されました。",
|
||||
"inviteURLFirstPartPersonal": "{{name}} があなたをミーティングに招待しました。\n",
|
||||
"inviteURLSecondPart": "\nミーティングにご参加ください:\n{{url}}\n",
|
||||
"liveStreamURL": "ライブストリーム:",
|
||||
"moreNumbers": "その他の番号",
|
||||
"noNumbers": "ダイヤルイン番号はありません。",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -639,7 +639,7 @@
|
||||
"raiseHand": "Podnieś / Opuść rękę",
|
||||
"raiseYourHand": "Podnieś rękę",
|
||||
"Settings": "Ustawienia",
|
||||
"sharedvideo": "Udostępnij wideo w Youtube",
|
||||
"sharedvideo": "Udostępnij wideo z Youtube",
|
||||
"shareRoom": "Zaproś kogoś",
|
||||
"shortcuts": "Wyświetl skróty",
|
||||
"speakerStats": "Statystyki mówców",
|
||||
|
||||
@@ -71,9 +71,8 @@
|
||||
},
|
||||
"privateNotice": "{{recipient}} için özel mesaj",
|
||||
"title": "Sohbet",
|
||||
"you": "sen",
|
||||
|
||||
|
||||
"you": "sen"
|
||||
|
||||
},
|
||||
"connectingOverlay": {
|
||||
"joiningRoom": "Toplantıya bağlanılıyor..."
|
||||
|
||||
@@ -110,6 +110,7 @@
|
||||
"localaddress_plural": "Local addresses:",
|
||||
"localport": "Local port:",
|
||||
"localport_plural": "Local ports:",
|
||||
"maxEnabledResolution": "send max",
|
||||
"more": "Show more",
|
||||
"packetloss": "Packet loss:",
|
||||
"quality": {
|
||||
@@ -202,6 +203,8 @@
|
||||
"enterDisplayName": "Please enter your name here",
|
||||
"error": "Error",
|
||||
"gracefulShutdown": "Our service is currently down for maintenance. Please try again later.",
|
||||
"grantModeratorDialog": "Are you sure you want to make this participant a moderator?",
|
||||
"grantModeratorTitle": "Grant moderator",
|
||||
"IamHost": "I am the host",
|
||||
"incorrectRoomLockPassword": "Incorrect password",
|
||||
"incorrectPassword": "Incorrect username or password",
|
||||
@@ -668,6 +671,7 @@
|
||||
"e2ee": "End-to-End Encryption",
|
||||
"feedback": "Leave feedback",
|
||||
"fullScreen": "Toggle full screen",
|
||||
"grantModerator": "Grant Moderator",
|
||||
"hangup": "Leave the call",
|
||||
"help": "Help",
|
||||
"invite": "Invite people",
|
||||
@@ -816,6 +820,7 @@
|
||||
"domute": "Mute",
|
||||
"domuteOthers": "Mute everyone else",
|
||||
"flip": "Flip",
|
||||
"grantModerator": "Grant Moderator",
|
||||
"kick": "Kick out",
|
||||
"moderator": "Moderator",
|
||||
"mute": "Participant is muted",
|
||||
@@ -864,6 +869,7 @@
|
||||
"header": "Help center"
|
||||
},
|
||||
"lobby": {
|
||||
"knockingParticipantList" : "Knocking participant list",
|
||||
"allow": "Allow",
|
||||
"backToKnockModeButton": "No password, ask to join instead",
|
||||
"dialogTitle": "Lobby mode",
|
||||
@@ -885,6 +891,11 @@
|
||||
"knockButton": "Ask to Join",
|
||||
"knockTitle": "Someone wants to join the meeting",
|
||||
"nameField": "Enter your name",
|
||||
"notificationLobbyAccessDenied": "{{targetParticipantName}} has been rejected to join by {{originParticipantName}}",
|
||||
"notificationLobbyAccessGranted": "{{targetParticipantName}} has been allowed to join by {{originParticipantName}}",
|
||||
"notificationLobbyDisabled": "Lobby has been disabled by {{originParticipantName}}",
|
||||
"notificationLobbyEnabled": "Lobby has been enabled by {{originParticipantName}}",
|
||||
"notificationTitle": "Lobby",
|
||||
"passwordField": "Enter meeting password",
|
||||
"passwordJoinButton": "Join",
|
||||
"reject": "Reject",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* global $, APP */
|
||||
|
||||
import { jitsiLocalStorage } from '@jitsi/js-utils';
|
||||
import Logger from 'jitsi-meet-logger';
|
||||
import { jitsiLocalStorage } from 'js-utils';
|
||||
|
||||
import {
|
||||
NOTIFICATION_TIMEOUT,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// FIXME: change to '../API' when we update to webpack2. If we do this now all
|
||||
// files from API modules will be included in external_api.js.
|
||||
import { PostMessageTransportBackend, Transport } from 'js-utils/transport';
|
||||
import { PostMessageTransportBackend, Transport } from '@jitsi/js-utils/transport';
|
||||
|
||||
import { getJitsiMeetGlobalNS } from '../../react/features/base/util';
|
||||
import { API_ID } from '../API/constants';
|
||||
|
||||
59
package-lock.json
generated
59
package-lock.json
generated
@@ -2832,6 +2832,23 @@
|
||||
"@types/yargs": "^13.0.0"
|
||||
}
|
||||
},
|
||||
"@jitsi/js-utils": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@jitsi/js-utils/-/js-utils-1.0.0.tgz",
|
||||
"integrity": "sha512-at9GPMP7IL0v6QS1Gs9c5MbbiN2AT0uKzsgKM8qS2wqXxqvpfT3p4U3+LH2IUyXiHlkmvlBMcNM02MltBdyRmQ==",
|
||||
"requires": {
|
||||
"bowser": "2.7.0",
|
||||
"js-md5": "0.7.3",
|
||||
"postis": "2.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"js-md5": {
|
||||
"version": "0.7.3",
|
||||
"resolved": "https://registry.npmjs.org/js-md5/-/js-md5-0.7.3.tgz",
|
||||
"integrity": "sha512-ZC41vPSTLKGwIRjqDh8DfXoCrdQIyBgspJVPXHBGu4nZlAEvG3nf+jO9avM9RmLiGakg7vz974ms99nEV0tmTQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@jitsi/sdp-interop": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@jitsi/sdp-interop/-/sdp-interop-1.0.3.tgz",
|
||||
@@ -10538,22 +10555,6 @@
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
|
||||
"integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls="
|
||||
},
|
||||
"js-utils": {
|
||||
"version": "github:jitsi/js-utils#cf11996bd866fdb47326c59a5d3bc24be17282d4",
|
||||
"from": "github:jitsi/js-utils#cf11996bd866fdb47326c59a5d3bc24be17282d4",
|
||||
"requires": {
|
||||
"bowser": "2.7.0",
|
||||
"js-md5": "0.7.3",
|
||||
"postis": "2.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"js-md5": {
|
||||
"version": "0.7.3",
|
||||
"resolved": "https://registry.npmjs.org/js-md5/-/js-md5-0.7.3.tgz",
|
||||
"integrity": "sha512-ZC41vPSTLKGwIRjqDh8DfXoCrdQIyBgspJVPXHBGu4nZlAEvG3nf+jO9avM9RmLiGakg7vz974ms99nEV0tmTQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"jsbn": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
|
||||
@@ -10724,15 +10725,16 @@
|
||||
}
|
||||
},
|
||||
"lib-jitsi-meet": {
|
||||
"version": "github:jitsi/lib-jitsi-meet#8f9bd254bb3813808e6e1f7974aacc4d1414fcdb",
|
||||
"from": "github:jitsi/lib-jitsi-meet#8f9bd254bb3813808e6e1f7974aacc4d1414fcdb",
|
||||
"version": "github:jitsi/lib-jitsi-meet#ff1813cbb2e7d36f1cc0bbc2b8baa0a16c6346a4",
|
||||
"from": "github:jitsi/lib-jitsi-meet#ff1813cbb2e7d36f1cc0bbc2b8baa0a16c6346a4",
|
||||
"requires": {
|
||||
"@jitsi/js-utils": "1.0.0",
|
||||
"@jitsi/sdp-interop": "1.0.3",
|
||||
"@jitsi/sdp-simulcast": "0.3.0",
|
||||
"async": "0.9.0",
|
||||
"current-executing-script": "0.1.3",
|
||||
"jitsi-meet-logger": "github:jitsi/jitsi-meet-logger#5ec92357570dc8f0b7ffc1528820721c84c6af8b",
|
||||
"js-utils": "github:jitsi/js-utils#cf11996bd866fdb47326c59a5d3bc24be17282d4",
|
||||
"lodash.clonedeep": "4.5.0",
|
||||
"lodash.isequal": "4.5.0",
|
||||
"sdp-transform": "2.3.0",
|
||||
"strophe.js": "1.3.4",
|
||||
@@ -14319,9 +14321,9 @@
|
||||
"integrity": "sha512-iqdJ1KpZbR4XGahgVmaeibB7kDhyMT7wrylINgJaYBY38IAiI0LF32VX1umO4pko6n21YF5I/kSeNQ+OXGqqow=="
|
||||
},
|
||||
"react-native-webrtc": {
|
||||
"version": "1.75.3",
|
||||
"resolved": "https://registry.npmjs.org/react-native-webrtc/-/react-native-webrtc-1.75.3.tgz",
|
||||
"integrity": "sha512-WIQu7jpH4osb4IMdjkpr62UYwt9kHIU4vWvvTHMxSqkGZwMGpTyFW/E+Y0T7If1azAvhq3GBuQjI3npxIXGbSQ==",
|
||||
"version": "1.84.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-webrtc/-/react-native-webrtc-1.84.0.tgz",
|
||||
"integrity": "sha512-xPOFbrcehuBzLnFy3keCM2HyMsyCVDQjQNAn8SIHKH/PA8Q7kZ4spuytc2E1hBTr7zH/vQ2Px+DWqu7on12jag==",
|
||||
"requires": {
|
||||
"base64-js": "^1.1.2",
|
||||
"event-target-shim": "^1.0.5",
|
||||
@@ -14330,9 +14332,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"uuid": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz",
|
||||
"integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ=="
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
|
||||
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -15001,6 +15003,13 @@
|
||||
"sdp": "^2.6.0"
|
||||
}
|
||||
},
|
||||
"rtcstats": {
|
||||
"version": "github:jitsi/rtcstats#02a1a089d9a97d1414d216ff7d9c432253e50190",
|
||||
"from": "github:jitsi/rtcstats#v6.1.3",
|
||||
"requires": {
|
||||
"@jitsi/js-utils": "1.0.0"
|
||||
}
|
||||
},
|
||||
"run-async": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz",
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"@atlaskit/theme": "7.0.2",
|
||||
"@atlaskit/toggle": "5.0.14",
|
||||
"@atlaskit/tooltip": "12.1.13",
|
||||
"@jitsi/js-utils": "1.0.0",
|
||||
"@microsoft/microsoft-graph-client": "1.1.0",
|
||||
"@react-native-community/async-storage": "1.3.4",
|
||||
"@react-native-community/google-signin": "3.0.1",
|
||||
@@ -54,9 +55,8 @@
|
||||
"jquery-contextmenu": "2.4.5",
|
||||
"jquery-i18next": "1.2.1",
|
||||
"js-md5": "0.6.1",
|
||||
"js-utils": "github:jitsi/js-utils#cf11996bd866fdb47326c59a5d3bc24be17282d4",
|
||||
"jwt-decode": "2.2.0",
|
||||
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#8f9bd254bb3813808e6e1f7974aacc4d1414fcdb",
|
||||
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#ff1813cbb2e7d36f1cc0bbc2b8baa0a16c6346a4",
|
||||
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
|
||||
"lodash": "4.17.13",
|
||||
"moment": "2.19.4",
|
||||
@@ -81,7 +81,7 @@
|
||||
"react-native-svg-transformer": "0.13.0",
|
||||
"react-native-swipeout": "2.3.6",
|
||||
"react-native-watch-connectivity": "0.4.3",
|
||||
"react-native-webrtc": "1.75.3",
|
||||
"react-native-webrtc": "1.84.0",
|
||||
"react-native-webview": "7.4.1",
|
||||
"react-native-youtube-iframe": "1.2.3",
|
||||
"react-redux": "7.1.0",
|
||||
@@ -90,6 +90,7 @@
|
||||
"redux": "4.0.4",
|
||||
"redux-thunk": "2.2.0",
|
||||
"rnnoise-wasm": "github:jitsi/rnnoise-wasm.git#566a16885897704d6e6d67a1d5ac5d39781db2af",
|
||||
"rtcstats": "github:jitsi/rtcstats#v6.1.3",
|
||||
"styled-components": "3.4.9",
|
||||
"util": "0.12.1",
|
||||
"uuid": "3.1.0",
|
||||
|
||||
@@ -538,6 +538,26 @@ export function createRemoteVideoMenuButtonEvent(buttonName, attributes) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The rtcstats websocket onclose event. We send this to amplitude in order
|
||||
* to detect trace ws prematurely closing.
|
||||
*
|
||||
* @param {Object} closeEvent - The event with which the websocket closed.
|
||||
* @returns {Object} The event in a format suitable for sending via
|
||||
* sendAnalytics.
|
||||
*/
|
||||
export function createRTCStatsTraceCloseEvent(closeEvent) {
|
||||
const event = {
|
||||
action: 'trace.onclose',
|
||||
source: 'rtcstats'
|
||||
};
|
||||
|
||||
event.code = closeEvent.code;
|
||||
event.reason = closeEvent.reason;
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event indicating that an action related to video blur
|
||||
* occurred (e.g. It was started or stopped).
|
||||
|
||||
@@ -30,6 +30,16 @@ export function sendAnalytics(event: Object) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return saved amplitude identity info such as session id, device id and user id. We assume these do not change for
|
||||
* the duration of the conference.
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function getAmplitudeIdentity() {
|
||||
return analytics.amplitudeIdentityProps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the analytics adapter to its initial state - removes handlers, cache,
|
||||
* disabled state, etc.
|
||||
@@ -92,6 +102,8 @@ export function createHandlers({ getState }: { getState: Function }) {
|
||||
try {
|
||||
const amplitude = new AmplitudeHandler(handlerConstructorOptions);
|
||||
|
||||
analytics.amplitudeIdentityProps = amplitude.getIdentityProps();
|
||||
|
||||
handlers.push(amplitude);
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch (e) {}
|
||||
@@ -117,7 +129,9 @@ export function createHandlers({ getState }: { getState: Function }) {
|
||||
})
|
||||
.catch(e => {
|
||||
analytics.dispose();
|
||||
logger.error(e);
|
||||
if (handlers.length !== 0) {
|
||||
logger.error(e);
|
||||
}
|
||||
|
||||
return [];
|
||||
}));
|
||||
|
||||
@@ -65,4 +65,17 @@ export default class AmplitudeHandler extends AbstractHandler {
|
||||
this._extractName(event),
|
||||
event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return amplitude identity information.
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
getIdentityProps() {
|
||||
return {
|
||||
sessionId: amplitude.getInstance(this._amplitudeOptions).getSessionId(),
|
||||
deviceId: amplitude.getInstance(this._amplitudeOptions).options.deviceId,
|
||||
userId: amplitude.getInstance(this._amplitudeOptions).options.userId
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @flow
|
||||
|
||||
import { generateRoomWithoutSeparator } from 'js-utils/random';
|
||||
import { generateRoomWithoutSeparator } from '@jitsi/js-utils/random';
|
||||
import type { Component } from 'react';
|
||||
|
||||
import { isRoomValid } from '../base/conference';
|
||||
|
||||
@@ -37,10 +37,12 @@ import '../recent-list/middleware';
|
||||
import '../recording/middleware';
|
||||
import '../rejoin/middleware';
|
||||
import '../room-lock/middleware';
|
||||
import '../rtcstats/middleware';
|
||||
import '../subtitles/middleware';
|
||||
import '../toolbox/middleware';
|
||||
import '../transcribing/middleware';
|
||||
import '../video-layout/middleware';
|
||||
import '../video-quality/middleware';
|
||||
import '../videosipgw/middleware';
|
||||
|
||||
import './middleware';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @flow
|
||||
|
||||
import { jitsiLocalStorage } from 'js-utils';
|
||||
import { jitsiLocalStorage } from '@jitsi/js-utils';
|
||||
import _ from 'lodash';
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
@@ -122,14 +122,14 @@ export default class BaseApp extends Component<*, State> {
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { route: { component }, store } = this.state;
|
||||
const { route: { component, props }, store } = this.state;
|
||||
|
||||
if (store) {
|
||||
return (
|
||||
<I18nextProvider i18n = { i18next }>
|
||||
<Provider store = { store }>
|
||||
<Fragment>
|
||||
{ this._createMainElement(component) }
|
||||
{ this._createMainElement(component, props) }
|
||||
<SoundCollection />
|
||||
{ this._createExtraElement() }
|
||||
{ this._renderDialogContainer() }
|
||||
|
||||
@@ -117,14 +117,15 @@ StateListenerRegistry.register(
|
||||
maxReceiverVideoQuality,
|
||||
preferredVideoQuality
|
||||
} = currentState;
|
||||
const changedConference = conference !== previousState.conference;
|
||||
const changedPreferredVideoQuality
|
||||
= preferredVideoQuality !== previousState.preferredVideoQuality;
|
||||
const changedMaxVideoQuality = maxReceiverVideoQuality !== previousState.maxReceiverVideoQuality;
|
||||
|
||||
if (changedPreferredVideoQuality || changedMaxVideoQuality) {
|
||||
if (changedConference || changedPreferredVideoQuality || changedMaxVideoQuality) {
|
||||
_setReceiverVideoConstraint(conference, preferredVideoQuality, maxReceiverVideoQuality);
|
||||
}
|
||||
if (changedPreferredVideoQuality) {
|
||||
if (changedConference || changedPreferredVideoQuality) {
|
||||
_setSenderVideoConstraint(conference, preferredVideoQuality);
|
||||
}
|
||||
});
|
||||
@@ -460,7 +461,10 @@ function _sendTones({ getState }, next, action) {
|
||||
*/
|
||||
function _setReceiverVideoConstraint(conference, preferred, max) {
|
||||
if (conference) {
|
||||
conference.setReceiverVideoConstraint(Math.min(preferred, max));
|
||||
const value = Math.min(preferred, max);
|
||||
|
||||
conference.setReceiverVideoConstraint(value);
|
||||
logger.info(`setReceiverVideoConstraint: ${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @flow
|
||||
|
||||
import { jitsiLocalStorage } from 'js-utils';
|
||||
import { jitsiLocalStorage } from '@jitsi/js-utils';
|
||||
import type { Dispatch } from 'redux';
|
||||
|
||||
import { addKnownDomains } from '../known-domains';
|
||||
|
||||
@@ -125,6 +125,7 @@ export default [
|
||||
'minParticipants',
|
||||
'nick',
|
||||
'openBridgeChannel',
|
||||
'opusMaxAvgBitrate',
|
||||
'p2p',
|
||||
'pcStatsInterval',
|
||||
'preferH264',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @flow
|
||||
|
||||
import { jitsiLocalStorage } from 'js-utils';
|
||||
import { jitsiLocalStorage } from '@jitsi/js-utils';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { parseURLParams } from '../util';
|
||||
|
||||
@@ -24,6 +24,7 @@ export default [
|
||||
'DISABLE_TRANSCRIPTION_SUBTITLES',
|
||||
'DISABLE_VIDEO_BACKGROUND',
|
||||
'DISPLAY_WELCOME_PAGE_CONTENT',
|
||||
'ENABLE_DIAL_OUT',
|
||||
'ENABLE_FEEDBACK_ANIMATION',
|
||||
'ENFORCE_NOTIFICATION_AUTO_DISMISS_TIMEOUT',
|
||||
'FILM_STRIP_MAX_HEIGHT',
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// @flow
|
||||
|
||||
import { jitsiLocalStorage } from 'js-utils';
|
||||
import { jitsiLocalStorage } from '@jitsi/js-utils';
|
||||
|
||||
import { APP_WILL_MOUNT } from '../app';
|
||||
import { getFeatureFlag } from '../flags/functions';
|
||||
import { addKnownDomains } from '../known-domains';
|
||||
import { MiddlewareRegistry } from '../redux';
|
||||
import { parseURIString } from '../util';
|
||||
@@ -107,6 +108,12 @@ function _setConfig({ dispatch, getState }, next, action) {
|
||||
config.p2p = { enabled: !settings.disableP2P };
|
||||
}
|
||||
|
||||
const resolutionFlag = getFeatureFlag(state, 'resolution');
|
||||
|
||||
if (typeof resolutionFlag !== 'undefined') {
|
||||
config.resolution = resolutionFlag;
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: _UPDATE_CONFIG,
|
||||
config
|
||||
|
||||
@@ -15,14 +15,19 @@ import {
|
||||
NOTIFY_CAMERA_ERROR,
|
||||
NOTIFY_MIC_ERROR,
|
||||
SET_AUDIO_INPUT_DEVICE,
|
||||
SET_VIDEO_INPUT_DEVICE
|
||||
SET_VIDEO_INPUT_DEVICE,
|
||||
UPDATE_DEVICE_LIST
|
||||
} from './actionTypes';
|
||||
import {
|
||||
removePendingDeviceRequests,
|
||||
setAudioInputDevice,
|
||||
setVideoInputDevice
|
||||
} from './actions';
|
||||
import { formatDeviceLabel, setAudioOutputDeviceId } from './functions';
|
||||
import {
|
||||
formatDeviceLabel,
|
||||
groupDevicesByKind,
|
||||
setAudioOutputDeviceId
|
||||
} from './functions';
|
||||
import logger from './logger';
|
||||
|
||||
const JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP = {
|
||||
@@ -41,6 +46,24 @@ const JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP = {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Logs the current device list.
|
||||
*
|
||||
* @param {Object} deviceList - Whatever is returned by {@link groupDevicesByKind}.
|
||||
* @returns {string}
|
||||
*/
|
||||
function logDeviceList(deviceList) {
|
||||
const devicesToStr = list => list.map(device => `\t\t${device.label}[${device.deviceId}]`).join('\n');
|
||||
const audioInputs = devicesToStr(deviceList.audioInput);
|
||||
const audioOutputs = devicesToStr(deviceList.audioOutput);
|
||||
const videoInputs = devicesToStr(deviceList.videoInput);
|
||||
|
||||
logger.debug('Device list updated:\n'
|
||||
+ `audioInput:\n${audioInputs}\n`
|
||||
+ `audioOutput:\n${audioOutputs}\n`
|
||||
+ `videoInput:\n${videoInputs}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the middleware of the feature base/devices.
|
||||
*
|
||||
@@ -123,6 +146,9 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
APP.UI.emitEvent(UIEvents.VIDEO_DEVICE_CHANGED, action.deviceId);
|
||||
}
|
||||
break;
|
||||
case UPDATE_DEVICE_LIST:
|
||||
logDeviceList(groupDevicesByKind(action.devices));
|
||||
break;
|
||||
case CHECK_AND_NOTIFY_FOR_NEW_DEVICE:
|
||||
_checkAndNotifyForNewDevice(store, action.newDevices, action.oldDevices);
|
||||
break;
|
||||
|
||||
@@ -19,24 +19,6 @@ const DEFAULT_STATE = {
|
||||
pendingRequests: []
|
||||
};
|
||||
|
||||
/**
|
||||
* Logs the current device list.
|
||||
*
|
||||
* @param {Object} deviceList - Whatever is returned by {@link groupDevicesByKind}.
|
||||
* @returns {string}
|
||||
*/
|
||||
function logDeviceList(deviceList) {
|
||||
const devicesToStr = list => list.map(device => `\t\t${device.label}[${device.deviceId}]`).join('\n');
|
||||
const audioInputs = devicesToStr(deviceList.audioInput);
|
||||
const audioOutputs = devicesToStr(deviceList.audioOutput);
|
||||
const videoInputs = devicesToStr(deviceList.videoInput);
|
||||
|
||||
logger.debug('Device list updated:\n'
|
||||
+ `audioInput:\n${audioInputs}\n`
|
||||
+ `audioOutput:\n${audioOutputs}\n`
|
||||
+ `videoInput:\n${videoInputs}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen for actions which changes the state of known and used devices.
|
||||
*
|
||||
@@ -54,8 +36,6 @@ ReducerRegistry.register(
|
||||
case UPDATE_DEVICE_LIST: {
|
||||
const deviceList = groupDevicesByKind(action.devices);
|
||||
|
||||
logDeviceList(deviceList);
|
||||
|
||||
return {
|
||||
...state,
|
||||
availableDevices: deviceList
|
||||
|
||||
@@ -25,6 +25,12 @@ export const CALL_INTEGRATION_ENABLED = 'call-integration.enabled';
|
||||
*/
|
||||
export const CLOSE_CAPTIONS_ENABLED = 'close-captions.enabled';
|
||||
|
||||
/**
|
||||
* Flag indicating if conference timer should be enabled.
|
||||
* Default: enabled (true).
|
||||
*/
|
||||
export const CONFERENCE_TIMER_ENABLED = 'conference-timer.enabled';
|
||||
|
||||
/**
|
||||
* Flag indicating if chat should be enabled.
|
||||
* Default: enabled (true).
|
||||
@@ -81,6 +87,13 @@ export const RAISE_HAND_ENABLED = 'raise-hand.enabled';
|
||||
*/
|
||||
export const RECORDING_ENABLED = 'recording.enabled';
|
||||
|
||||
/**
|
||||
* Flag indicating the local and (maximum) remote video resolution. Overrides
|
||||
* the server configuration.
|
||||
* Default: (unset).
|
||||
*/
|
||||
export const RESOLUTION = 'resolution';
|
||||
|
||||
/**
|
||||
* Flag indicating if server URL change is enabled.
|
||||
* Default: enabled (true)
|
||||
@@ -99,6 +112,12 @@ export const TILE_VIEW_ENABLED = 'tile-view.enabled';
|
||||
*/
|
||||
export const TOOLBOX_ALWAYS_VISIBLE = 'toolbox.alwaysVisible';
|
||||
|
||||
/**
|
||||
* Flag indicating if the video share button should be enabled
|
||||
* Default: enabled (true).
|
||||
*/
|
||||
export const VIDEO_SHARE_BUTTON_ENABLED = 'video-share.enabled';
|
||||
|
||||
/**
|
||||
* Flag indicating if the welcome page should be enabled.
|
||||
* Default: disabled (false).
|
||||
|
||||
3
react/features/base/icons/svg/crown.svg
Normal file
3
react/features/base/icons/svg/crown.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14 4C14 4.85739 13.4605 5.58876 12.7024 5.87317L14.2286 9.94296L14.9455 11.8546C15.0074 11.9292 15.0708 11.9292 15.1098 11.8902L16.5535 10.4465L18.5858 8.41421C18.2239 8.05228 18 7.55228 18 7C18 5.89543 18.8954 5 20 5C21.1046 5 22 5.89543 22 7C22 8.10457 21.1046 9 20 9C19.9441 9 19.8887 8.9977 19.8339 8.9932L19 19C19 20.1046 18.1046 21 17 21H7C5.89543 21 5 20.1046 5 19L4.1661 8.9932C4.11133 8.9977 4.05593 9 4 9C2.89543 9 2 8.10457 2 7C2 5.89543 2.89543 5 4 5C5.10457 5 6 5.89543 6 7C6 7.55228 5.77614 8.05228 5.41421 8.41421L7.44654 10.4465L8.89019 11.8902C8.9775 11.9325 9.03514 11.9063 9.05453 11.8546L9.77139 9.94296L11.2976 5.87317C10.5395 5.58876 10 4.85739 10 4C10 2.89543 10.8954 2 12 2C13.1046 2 14 2.89543 14 4ZM6.84027 17L6.44651 12.2749L7.47597 13.3044C7.68795 13.5164 7.94285 13.6805 8.22354 13.7858C9.30949 14.193 10.52 13.6428 10.9272 12.5568L12 9.696L13.0728 12.5568C13.1781 12.8375 13.3422 13.0924 13.5542 13.3044C14.3743 14.1245 15.7039 14.1245 16.524 13.3044L17.5535 12.2749L17.1597 17H6.84027Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -23,6 +23,7 @@ export { default as IconClosedCaption } from './closed_caption.svg';
|
||||
export { default as IconConnectionActive } from './gsm-bars.svg';
|
||||
export { default as IconConnectionInactive } from './ninja.svg';
|
||||
export { default as IconCopy } from './copy.svg';
|
||||
export { default as IconCrown } from './crown.svg';
|
||||
export { default as IconDeviceBluetooth } from './bluetooth.svg';
|
||||
export { default as IconDeviceEarpiece } from './phone-talk.svg';
|
||||
export { default as IconDeviceHeadphone } from './headset.svg';
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
import type { Dispatch } from 'redux';
|
||||
|
||||
import { isOnline } from '../net-info/selectors';
|
||||
|
||||
import JitsiMeetJS from './_';
|
||||
import {
|
||||
LIB_DID_DISPOSE,
|
||||
@@ -37,7 +39,8 @@ export function disposeLib() {
|
||||
*/
|
||||
export function initLib() {
|
||||
return (dispatch: Dispatch<any>, getState: Function): void => {
|
||||
const config = getState()['features/base/config'];
|
||||
const state = getState();
|
||||
const config = state['features/base/config'];
|
||||
|
||||
if (!config) {
|
||||
throw new Error('Cannot init lib-jitsi-meet without config');
|
||||
@@ -50,6 +53,9 @@ export function initLib() {
|
||||
enableAnalyticsLogging: isAnalyticsEnabled(getState),
|
||||
...config
|
||||
});
|
||||
JitsiMeetJS.setNetworkInfo({
|
||||
isOnline: isOnline(state)
|
||||
});
|
||||
dispatch({ type: LIB_DID_INIT });
|
||||
} catch (error) {
|
||||
dispatch(libInitError(error));
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { SET_CONFIG } from '../config';
|
||||
import { setLoggingConfig } from '../logging';
|
||||
import { SET_NETWORK_INFO } from '../net-info';
|
||||
import { PARTICIPANT_LEFT } from '../participants';
|
||||
import { MiddlewareRegistry } from '../redux';
|
||||
|
||||
@@ -31,6 +32,12 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
}
|
||||
break;
|
||||
|
||||
case SET_NETWORK_INFO:
|
||||
JitsiMeetJS.setNetworkInfo({
|
||||
isOnline: action.isOnline
|
||||
});
|
||||
break;
|
||||
|
||||
case PARTICIPANT_LEFT:
|
||||
action.participant.local && store.dispatch(disposeLib());
|
||||
break;
|
||||
|
||||
11
react/features/base/net-info/selectors.js
Normal file
11
react/features/base/net-info/selectors.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { STORE_NAME } from './constants';
|
||||
|
||||
/**
|
||||
* A selector for the internet online status.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isOnline(state) {
|
||||
return state[STORE_NAME].isOnline;
|
||||
}
|
||||
@@ -12,6 +12,16 @@
|
||||
*/
|
||||
export const DOMINANT_SPEAKER_CHANGED = 'DOMINANT_SPEAKER_CHANGED';
|
||||
|
||||
/**
|
||||
* Create an action for granting moderator to a participant.
|
||||
*
|
||||
* {
|
||||
* type: GRANT_MODERATOR,
|
||||
* id: string
|
||||
* }
|
||||
*/
|
||||
export const GRANT_MODERATOR = 'GRANT_MODERATOR';
|
||||
|
||||
/**
|
||||
* Create an action for removing a participant from the conference.
|
||||
*
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
DOMINANT_SPEAKER_CHANGED,
|
||||
HIDDEN_PARTICIPANT_JOINED,
|
||||
HIDDEN_PARTICIPANT_LEFT,
|
||||
GRANT_MODERATOR,
|
||||
KICK_PARTICIPANT,
|
||||
MUTE_REMOTE_PARTICIPANT,
|
||||
PARTICIPANT_ID_CHANGED,
|
||||
@@ -47,6 +48,22 @@ export function dominantSpeakerChanged(id, conference) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an action for granting moderator to a participant.
|
||||
*
|
||||
* @param {string} id - Participant's ID.
|
||||
* @returns {{
|
||||
* type: GRANT_MODERATOR,
|
||||
* id: string
|
||||
* }}
|
||||
*/
|
||||
export function grantModerator(id) {
|
||||
return {
|
||||
type: GRANT_MODERATOR,
|
||||
id
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an action for removing a participant from the conference.
|
||||
*
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// @flow
|
||||
import { getGravatarURL } from 'js-utils/avatar';
|
||||
|
||||
import { getGravatarURL } from '@jitsi/js-utils/avatar';
|
||||
|
||||
import { JitsiParticipantConnectionStatus } from '../lib-jitsi-meet';
|
||||
import { MEDIA_TYPE, shouldRenderVideoTrack } from '../media';
|
||||
@@ -258,6 +259,16 @@ export function getYoutubeParticipant(stateful: Object | Function) {
|
||||
return participants.filter(p => p.isFakeParticipant)[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the participant is a moderator.
|
||||
*
|
||||
* @param {string} participant - Participant object.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isParticipantModerator(participant: Object) {
|
||||
return participant?.role === PARTICIPANT_ROLE.MODERATOR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if all of the meeting participants are moderators.
|
||||
*
|
||||
@@ -268,13 +279,7 @@ export function getYoutubeParticipant(stateful: Object | Function) {
|
||||
export function isEveryoneModerator(stateful: Object | Function) {
|
||||
const participants = _getAllParticipants(stateful);
|
||||
|
||||
for (const participant of participants) {
|
||||
if (participant.role !== PARTICIPANT_ROLE.MODERATOR) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return participants.every(isParticipantModerator);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,6 +15,7 @@ import { playSound, registerSound, unregisterSound } from '../sounds';
|
||||
|
||||
import {
|
||||
DOMINANT_SPEAKER_CHANGED,
|
||||
GRANT_MODERATOR,
|
||||
KICK_PARTICIPANT,
|
||||
MUTE_REMOTE_PARTICIPANT,
|
||||
PARTICIPANT_DISPLAY_NAME_CHANGED,
|
||||
@@ -86,6 +87,13 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
break;
|
||||
}
|
||||
|
||||
case GRANT_MODERATOR: {
|
||||
const { conference } = store.getState()['features/base/conference'];
|
||||
|
||||
conference.grantOwner(action.id);
|
||||
break;
|
||||
}
|
||||
|
||||
case KICK_PARTICIPANT: {
|
||||
const { conference } = store.getState()['features/base/conference'];
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { getCurrentConferenceUrl } from '../../../connection';
|
||||
import { translate } from '../../../i18n';
|
||||
import { Icon, IconCopy, IconCheck } from '../../../icons';
|
||||
import { connect } from '../../../redux';
|
||||
import { copyText } from '../../../util';
|
||||
import { copyText, getDecodedURI } from '../../../util';
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -156,7 +156,7 @@ class CopyMeetingUrl extends Component<Props, State> {
|
||||
className = { `url ${showLinkCopied ? 'done' : ''}` }
|
||||
onClick = { _copyUrl } >
|
||||
<div className = 'copy-meeting-text'>
|
||||
{ !showCopyLink && !showLinkCopied && url }
|
||||
{ !showCopyLink && !showLinkCopied && getDecodedURI(url) }
|
||||
{ showCopyLink && t('prejoin.copyAndShare') }
|
||||
{ showLinkCopied && t('prejoin.linkCopied') }
|
||||
</div>
|
||||
|
||||
@@ -19,11 +19,31 @@ type Props = {
|
||||
*/
|
||||
footer?: React$Node,
|
||||
|
||||
/**
|
||||
* The name of the participant.
|
||||
*/
|
||||
name?: string,
|
||||
|
||||
/**
|
||||
* Indicates whether the avatar should be shown when video is off
|
||||
*/
|
||||
showAvatar: boolean,
|
||||
|
||||
/**
|
||||
* Indicates whether the label and copy url action should be shown
|
||||
*/
|
||||
showConferenceInfo: boolean,
|
||||
|
||||
/**
|
||||
* Title of the screen.
|
||||
*/
|
||||
title: string,
|
||||
|
||||
/**
|
||||
* The 'Skip prejoin' button to be rendered (if any).
|
||||
*/
|
||||
skipPrejoinButton?: React$Node,
|
||||
|
||||
/**
|
||||
* True if the preview overlay should be muted, false otherwise.
|
||||
*/
|
||||
@@ -40,31 +60,49 @@ type Props = {
|
||||
* on the prejoin screen (pre-connection) or lobby (post-connection).
|
||||
*/
|
||||
export default class PreMeetingScreen extends PureComponent<Props> {
|
||||
/**
|
||||
* Default values for {@code Prejoin} component's properties.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static defaultProps = {
|
||||
showAvatar: true,
|
||||
showConferenceInfo: true
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements {@code PureComponent#render}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const { title, videoMuted, videoTrack } = this.props;
|
||||
const { name, showAvatar, showConferenceInfo, title, videoMuted, videoTrack } = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className = 'premeeting-screen'
|
||||
id = 'lobby-screen'>
|
||||
<Preview
|
||||
name = { name }
|
||||
showAvatar = { showAvatar }
|
||||
videoMuted = { videoMuted }
|
||||
videoTrack = { videoTrack } />
|
||||
{!videoMuted && <div className = 'preview-overlay' />}
|
||||
<div className = 'content'>
|
||||
<div className = 'title'>
|
||||
{ title }
|
||||
</div>
|
||||
<CopyMeetingUrl />
|
||||
{showConferenceInfo && (
|
||||
<>
|
||||
<div className = 'title'>
|
||||
{ title }
|
||||
</div>
|
||||
<CopyMeetingUrl />
|
||||
</>
|
||||
)}
|
||||
{ this.props.children }
|
||||
<div className = 'media-btn-container'>
|
||||
<AudioSettingsButton visible = { true } />
|
||||
<VideoSettingsButton visible = { true } />
|
||||
</div>
|
||||
{ this.props.skipPrejoinButton }
|
||||
{ this.props.footer }
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,6 +14,11 @@ export type Props = {
|
||||
*/
|
||||
name: string,
|
||||
|
||||
/**
|
||||
* Indicates whether the avatar should be shown when video is off
|
||||
*/
|
||||
showAvatar: boolean,
|
||||
|
||||
/**
|
||||
* Flag signaling the visibility of camera preview.
|
||||
*/
|
||||
@@ -32,7 +37,7 @@ export type Props = {
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
function Preview(props: Props) {
|
||||
const { name, videoMuted, videoTrack } = props;
|
||||
const { name, showAvatar, videoMuted, videoTrack } = props;
|
||||
|
||||
if (!videoMuted && videoTrack) {
|
||||
return (
|
||||
@@ -44,18 +49,29 @@ function Preview(props: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className = 'no-video'
|
||||
id = 'preview'>
|
||||
<Avatar
|
||||
className = 'preview-avatar'
|
||||
displayName = { name }
|
||||
size = { 200 } />
|
||||
</div>
|
||||
);
|
||||
if (showAvatar) {
|
||||
return (
|
||||
<div
|
||||
className = 'no-video'
|
||||
id = 'preview'>
|
||||
<div className = 'preview-avatar-container'>
|
||||
<Avatar
|
||||
className = 'preview-avatar'
|
||||
displayName = { name }
|
||||
participantId = 'local'
|
||||
size = { 200 } />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Preview.defaultProps = {
|
||||
showAvatar: true
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps part of the Redux state to the props of this component.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { Icon, IconCheck } from '../../../icons';
|
||||
|
||||
const mainClass = 'toggle-button';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Text of the button.
|
||||
*/
|
||||
children: React$Node,
|
||||
|
||||
/**
|
||||
* If the button is toggled or not.
|
||||
*/
|
||||
isToggled?: boolean,
|
||||
|
||||
/**
|
||||
* OnClick button handler.
|
||||
*/
|
||||
onClick: Function
|
||||
}
|
||||
|
||||
/**
|
||||
* Button used as a toggle.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
function ToggleButton({ children, isToggled, onClick }: Props) {
|
||||
const className = isToggled ? `${mainClass} ${mainClass}--toggled` : mainClass;
|
||||
|
||||
return (
|
||||
<div
|
||||
className = { className }
|
||||
onClick = { onClick }>
|
||||
<div className = 'toggle-button-container'>
|
||||
<div className = 'toggle-button-icon-container'>
|
||||
<Icon
|
||||
className = 'toggle-button-icon'
|
||||
size = { 10 }
|
||||
src = { IconCheck } />
|
||||
</div>
|
||||
<span>{children}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ToggleButton;
|
||||
@@ -3,3 +3,4 @@
|
||||
export { default as ActionButton } from './ActionButton';
|
||||
export { default as InputField } from './InputField';
|
||||
export { default as PreMeetingScreen } from './PreMeetingScreen';
|
||||
export { default as ToggleButton } from './ToggleButton';
|
||||
|
||||
@@ -47,6 +47,11 @@ type Props = {
|
||||
*/
|
||||
_welcomePageIsVisible: boolean,
|
||||
|
||||
/**
|
||||
* The default value for the Jitsi logo URL.
|
||||
*/
|
||||
defaultJitsiLogoURL: ?string,
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
@@ -218,13 +223,14 @@ class Watermarks extends Component<Props, State> {
|
||||
let reactElement = null;
|
||||
const {
|
||||
_customLogoUrl,
|
||||
_customLogoLink
|
||||
_customLogoLink,
|
||||
defaultJitsiLogoURL
|
||||
} = this.props;
|
||||
|
||||
if (this._canDisplayJitsiWatermark()) {
|
||||
const link = _customLogoLink || this.state.jitsiWatermarkLink;
|
||||
const style = {
|
||||
backgroundImage: `url(${_customLogoUrl || interfaceConfig.DEFAULT_LOGO_URL})`,
|
||||
backgroundImage: `url(${_customLogoUrl || defaultJitsiLogoURL || interfaceConfig.DEFAULT_LOGO_URL})`,
|
||||
maxWidth: 140,
|
||||
maxHeight: 70
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
|
||||
import { jitsiLocalStorage } from '@jitsi/js-utils';
|
||||
import md5 from 'js-md5';
|
||||
import { jitsiLocalStorage } from 'js-utils';
|
||||
|
||||
import logger from './logger';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
|
||||
import { jitsiLocalStorage } from 'js-utils';
|
||||
import { randomHexString } from 'js-utils/random';
|
||||
import { jitsiLocalStorage } from '@jitsi/js-utils';
|
||||
import { randomHexString } from '@jitsi/js-utils/random';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { APP_WILL_MOUNT } from '../app/actionTypes';
|
||||
|
||||
@@ -36,6 +36,11 @@ type Props = {
|
||||
*/
|
||||
_localUserId: string,
|
||||
|
||||
/**
|
||||
* The local participant's role.
|
||||
*/
|
||||
_localUserRole: string,
|
||||
|
||||
/**
|
||||
* Indicates whether or not the test mode is currently on. Otherwise the
|
||||
* TestConnectionInfo component will not render.
|
||||
@@ -179,6 +184,9 @@ class TestConnectionInfo extends Component<Props, State> {
|
||||
<TestHint
|
||||
id = 'org.jitsi.meet.conference.joinedState'
|
||||
value = { this.props._conferenceJoinedState } />
|
||||
<TestHint
|
||||
id = 'org.jitsi.meet.conference.localParticipantRole'
|
||||
value = { this.props._localUserRole } />
|
||||
<TestHint
|
||||
id = 'org.jitsi.meet.stats.rtp'
|
||||
value = { JSON.stringify(this.state.stats) } />
|
||||
@@ -208,7 +216,8 @@ function _mapStateToProps(state) {
|
||||
return {
|
||||
_conferenceConnectionState: state['features/testing'].connectionState,
|
||||
_conferenceJoinedState: conferenceJoined.toString(),
|
||||
_localUserId: localParticipant && localParticipant.id,
|
||||
_localUserId: localParticipant?.id,
|
||||
_localUserRole: localParticipant?.role,
|
||||
_testMode: isTestModeEnabled(state)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* global APP */
|
||||
|
||||
import JitsiMeetJS, { JitsiTrackErrors, browser } from '../lib-jitsi-meet';
|
||||
import { MEDIA_TYPE } from '../media';
|
||||
import { MEDIA_TYPE, setAudioMuted } from '../media';
|
||||
import {
|
||||
getUserSelectedCameraDeviceId,
|
||||
getUserSelectedMicDeviceId
|
||||
@@ -125,6 +125,89 @@ export function createLocalTracksF(options = {}, firePermissionPromptIsShownEven
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an object containing a promise which resolves with the created tracks &
|
||||
* the errors resulting from that process.
|
||||
*
|
||||
* @returns {Promise<JitsiLocalTrack>}
|
||||
*
|
||||
* @todo Refactor to not use APP
|
||||
*/
|
||||
export function createPrejoinTracks() {
|
||||
const errors = {};
|
||||
const initialDevices = [ 'audio' ];
|
||||
const requestedAudio = true;
|
||||
let requestedVideo = false;
|
||||
const { startAudioOnly, startWithAudioMuted, startWithVideoMuted } = APP.store.getState()['features/base/settings'];
|
||||
|
||||
// Always get a handle on the audio input device so that we have statistics even if the user joins the
|
||||
// conference muted. Previous implementation would only acquire the handle when the user first unmuted,
|
||||
// which would results in statistics ( such as "No audio input" or "Are you trying to speak?") being available
|
||||
// only after that point.
|
||||
if (startWithAudioMuted) {
|
||||
APP.store.dispatch(setAudioMuted(true));
|
||||
}
|
||||
|
||||
if (!startWithVideoMuted && !startAudioOnly) {
|
||||
initialDevices.push('video');
|
||||
requestedVideo = true;
|
||||
}
|
||||
|
||||
let tryCreateLocalTracks;
|
||||
|
||||
if (!requestedAudio && !requestedVideo) {
|
||||
// Resolve with no tracks
|
||||
tryCreateLocalTracks = Promise.resolve([]);
|
||||
} else {
|
||||
tryCreateLocalTracks = createLocalTracksF({ devices: initialDevices }, true)
|
||||
.catch(err => {
|
||||
if (requestedAudio && requestedVideo) {
|
||||
|
||||
// Try audio only...
|
||||
errors.audioAndVideoError = err;
|
||||
|
||||
return (
|
||||
createLocalTracksF({ devices: [ 'audio' ] }, true));
|
||||
} else if (requestedAudio && !requestedVideo) {
|
||||
errors.audioOnlyError = err;
|
||||
|
||||
return [];
|
||||
} else if (requestedVideo && !requestedAudio) {
|
||||
errors.videoOnlyError = err;
|
||||
|
||||
return [];
|
||||
}
|
||||
logger.error('Should never happen');
|
||||
})
|
||||
.catch(err => {
|
||||
// Log this just in case...
|
||||
if (!requestedAudio) {
|
||||
logger.error('The impossible just happened', err);
|
||||
}
|
||||
errors.audioOnlyError = err;
|
||||
|
||||
// Try video only...
|
||||
return requestedVideo
|
||||
? createLocalTracksF({ devices: [ 'video' ] }, true)
|
||||
: [];
|
||||
})
|
||||
.catch(err => {
|
||||
// Log this just in case...
|
||||
if (!requestedVideo) {
|
||||
logger.error('The impossible just happened', err);
|
||||
}
|
||||
errors.videoOnlyError = err;
|
||||
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
tryCreateLocalTracks,
|
||||
errors
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns local audio track.
|
||||
*
|
||||
|
||||
@@ -143,6 +143,10 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
if (typeof APP !== 'undefined') {
|
||||
const result = next(action);
|
||||
|
||||
if (isPrejoinPageVisible(store.getState())) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const { jitsiTrack } = action.track;
|
||||
const muted = jitsiTrack.isMuted();
|
||||
const participantID = jitsiTrack.getParticipantId();
|
||||
|
||||
@@ -591,3 +591,13 @@ export function addHashParamsToURL(url: URL, hashParamsToAdd: Object = {}) {
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the decoded URI.
|
||||
*
|
||||
* @param {string} uri - The URI to decode.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getDecodedURI(uri: string) {
|
||||
return decodeURI(uri.replace(/^https?:\/\//i, ''));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// @flow
|
||||
import { generateRoomWithoutSeparator } from 'js-utils/random';
|
||||
import { generateRoomWithoutSeparator } from '@jitsi/js-utils/random';
|
||||
import type { Dispatch } from 'redux';
|
||||
|
||||
import { getDefaultURL } from '../app/functions';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @flow
|
||||
|
||||
import { generateRoomWithoutSeparator } from 'js-utils/random';
|
||||
import { generateRoomWithoutSeparator } from '@jitsi/js-utils/random';
|
||||
import type { Dispatch } from 'redux';
|
||||
|
||||
import { createCalendarConnectedEvent, sendAnalytics } from '../analytics';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @flow
|
||||
|
||||
import { jitsiLocalStorage } from 'js-utils';
|
||||
import { jitsiLocalStorage } from '@jitsi/js-utils';
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
import {
|
||||
|
||||
@@ -126,11 +126,12 @@ class LonelyMeetingExperience extends PureComponent<Props> {
|
||||
*/
|
||||
function _mapStateToProps(state): $Shape<Props> {
|
||||
const { disableInviteFunctions } = state['features/base/config'];
|
||||
const { conference } = state['features/base/conference'];
|
||||
const flag = getFeatureFlag(state, INVITE_ENABLED, true);
|
||||
|
||||
return {
|
||||
_isInviteFunctionsDiabled: !flag || disableInviteFunctions,
|
||||
_isLonelyMeeting: getParticipantCount(state) === 1,
|
||||
_isLonelyMeeting: conference && getParticipantCount(state) === 1,
|
||||
_styles: ColorSchemeRegistry.get(state, 'Conference')
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { SafeAreaView, Text, View } from 'react-native';
|
||||
import LinearGradient from 'react-native-linear-gradient';
|
||||
|
||||
import { getConferenceName } from '../../../base/conference';
|
||||
import { getFeatureFlag, MEETING_NAME_ENABLED } from '../../../base/flags';
|
||||
import { getFeatureFlag, CONFERENCE_TIMER_ENABLED, MEETING_NAME_ENABLED } from '../../../base/flags';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { PictureInPictureButton } from '../../../mobile/picture-in-picture';
|
||||
import { isToolboxVisible } from '../../../toolbox';
|
||||
@@ -15,6 +15,11 @@ import styles, { NAVBAR_GRADIENT_COLORS } from './styles';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Whether displaying the current conference timer is enabled or not.
|
||||
*/
|
||||
_conferenceTimerEnabled: boolean,
|
||||
|
||||
/**
|
||||
* Name of the meeting we're currently in.
|
||||
*/
|
||||
@@ -73,7 +78,9 @@ class NavigationBar extends Component<Props> {
|
||||
{ this.props._meetingName }
|
||||
</Text>
|
||||
}
|
||||
<ConferenceTimer />
|
||||
{
|
||||
this.props._conferenceTimerEnabled && <ConferenceTimer />
|
||||
}
|
||||
</View>
|
||||
</View>
|
||||
];
|
||||
@@ -89,6 +96,7 @@ class NavigationBar extends Component<Props> {
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
return {
|
||||
_conferenceTimerEnabled: getFeatureFlag(state, CONFERENCE_TIMER_ENABLED, true),
|
||||
_meetingName: getConferenceName(state),
|
||||
_meetingNameEnabled: getFeatureFlag(state, MEETING_NAME_ENABLED, true),
|
||||
_visible: isToolboxVisible(state)
|
||||
|
||||
@@ -342,6 +342,7 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, State> {
|
||||
bridgeCount,
|
||||
e2eRtt,
|
||||
framerate,
|
||||
maxEnabledResolution,
|
||||
packetLoss,
|
||||
region,
|
||||
resolution,
|
||||
@@ -358,6 +359,7 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, State> {
|
||||
e2eRtt = { e2eRtt }
|
||||
framerate = { framerate }
|
||||
isLocalVideo = { this.props.isLocalVideo }
|
||||
maxEnabledResolution = { maxEnabledResolution }
|
||||
onShowMore = { this._onToggleShowMore }
|
||||
packetLoss = { packetLoss }
|
||||
region = { region }
|
||||
|
||||
@@ -57,6 +57,12 @@ type Props = {
|
||||
*/
|
||||
isLocalVideo: boolean,
|
||||
|
||||
/**
|
||||
* The send-side max enabled resolution (aka the highest layer that is not
|
||||
* suspended on the send-side).
|
||||
*/
|
||||
maxEnabledResolution: number,
|
||||
|
||||
/**
|
||||
* Callback to invoke when the show additional stats link is clicked.
|
||||
*/
|
||||
@@ -380,8 +386,8 @@ class ConnectionStatsTable extends Component<Props> {
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderResolution() {
|
||||
const { resolution, t } = this.props;
|
||||
const resolutionString = Object.keys(resolution || {})
|
||||
const { resolution, maxEnabledResolution, t } = this.props;
|
||||
let resolutionString = Object.keys(resolution || {})
|
||||
.map(ssrc => {
|
||||
const { width, height } = resolution[ssrc];
|
||||
|
||||
@@ -389,6 +395,12 @@ class ConnectionStatsTable extends Component<Props> {
|
||||
})
|
||||
.join(', ') || 'N/A';
|
||||
|
||||
if (maxEnabledResolution && maxEnabledResolution < 720) {
|
||||
const maxEnabledResolutionTitle = t('connectionindicator.maxEnabledResolution');
|
||||
|
||||
resolutionString += ` (${maxEnabledResolutionTitle} ${maxEnabledResolution}p)`;
|
||||
}
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
@@ -4,7 +4,7 @@ import React, { useState } from 'react';
|
||||
|
||||
import { translate } from '../../../../base/i18n';
|
||||
import { Icon, IconCheck, IconCopy } from '../../../../base/icons';
|
||||
import { copyText } from '../../../../base/util';
|
||||
import { copyText, getDecodedURI } from '../../../../base/util';
|
||||
|
||||
|
||||
type Props = {
|
||||
@@ -82,7 +82,7 @@ function CopyMeetingLinkSection({ t, url }: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
const displayUrl = decodeURI(url.replace(/^https?:\/\//i, ''));
|
||||
const displayUrl = getDecodedURI(url);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -40,7 +40,7 @@ class KnockingParticipantList extends AbstractKnockingParticipantList<Props> {
|
||||
className = { _toolboxVisible ? 'toolbox-visible' : '' }
|
||||
id = 'knocking-participant-list'>
|
||||
<span className = 'title'>
|
||||
Knocking participant list
|
||||
{ t('lobby.knockingParticipantList') }
|
||||
</span>
|
||||
<ul>
|
||||
{ _participants.map(p => (
|
||||
|
||||
@@ -1,6 +1,23 @@
|
||||
// @flow
|
||||
|
||||
import { getCurrentConference } from '../base/conference';
|
||||
import { toState } from '../base/redux';
|
||||
|
||||
const JID_PATTERN = '[^@]+@[^/]+/(.+)';
|
||||
|
||||
/**
|
||||
* Returns a knocking participant by ID or JID.
|
||||
*
|
||||
* @param {Function | Object} stateful - The Redux state or a function that resolves to the Redux state.
|
||||
* @param {string} id - The ID or JID of the participant.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function getKnockingParticipantById(stateful: Function | Object, id: string): Object {
|
||||
const { knockingParticipants } = toState(stateful)['features/lobby'];
|
||||
const idToFind = getIdFromJid(id) || id;
|
||||
|
||||
return knockingParticipants.find(p => p.id === idToFind);
|
||||
}
|
||||
|
||||
/**
|
||||
* Approves (lets in) or rejects a knocking participant.
|
||||
@@ -21,3 +38,15 @@ export function setKnockingParticipantApproval(getState: Function, id: string, a
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an ID from a JID, if a JID is provided, undefined otherwise.
|
||||
*
|
||||
* @param {string} jid - The JID to get the ID from.
|
||||
* @returns {?string}
|
||||
*/
|
||||
function getIdFromJid(jid: string): ?string {
|
||||
const match = new RegExp(JID_PATTERN, 'g').exec(jid) || [];
|
||||
|
||||
return match[1];
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { CONFERENCE_FAILED, CONFERENCE_JOINED } from '../base/conference';
|
||||
import { JitsiConferenceErrors, JitsiConferenceEvents } from '../base/lib-jitsi-meet';
|
||||
import { getFirstLoadableAvatarUrl } from '../base/participants';
|
||||
import { getFirstLoadableAvatarUrl, getParticipantDisplayName } from '../base/participants';
|
||||
import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
|
||||
import { NOTIFICATION_TYPE, showNotification } from '../notifications';
|
||||
import { isPrejoinPageEnabled } from '../prejoin/functions';
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
startKnocking,
|
||||
setPasswordJoinFailed
|
||||
} from './actions';
|
||||
import { getKnockingParticipantById } from './functions';
|
||||
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
switch (action.type) {
|
||||
@@ -43,7 +44,7 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
*/
|
||||
StateListenerRegistry.register(
|
||||
state => state['features/base/conference'].conference,
|
||||
(conference, { dispatch }, previousConference) => {
|
||||
(conference, { dispatch, getState }, previousConference) => {
|
||||
if (conference && !previousConference) {
|
||||
conference.on(JitsiConferenceEvents.MEMBERS_ONLY_CHANGED, enabled => {
|
||||
dispatch(setLobbyModeEnabled(enabled));
|
||||
@@ -66,6 +67,13 @@ StateListenerRegistry.register(
|
||||
conference.on(JitsiConferenceEvents.LOBBY_USER_LEFT, id => {
|
||||
dispatch(knockingParticipantLeft(id));
|
||||
});
|
||||
|
||||
conference.on(JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, (origin, sender) =>
|
||||
_maybeSendLobbyNotification(origin, sender, {
|
||||
dispatch,
|
||||
getState
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -151,3 +159,43 @@ function _findLoadableAvatarForKnockingParticipant({ dispatch, getState }, { id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the endpoint message that arrived through the conference and
|
||||
* sends a lobby notification, if the message belongs to the feature.
|
||||
*
|
||||
* @param {Object} origin - The origin (initiator) of the message.
|
||||
* @param {Object} message - The actual message.
|
||||
* @param {Object} store - The Redux store.
|
||||
* @returns {void}
|
||||
*/
|
||||
function _maybeSendLobbyNotification(origin, message, { dispatch, getState }) {
|
||||
if (!origin?._id || message?.type !== 'lobby-notify') {
|
||||
return;
|
||||
}
|
||||
|
||||
const notificationProps: any = {
|
||||
descriptionArguments: {
|
||||
originParticipantName: getParticipantDisplayName(getState, origin._id)
|
||||
},
|
||||
titleKey: 'lobby.notificationTitle'
|
||||
};
|
||||
|
||||
switch (message.event) {
|
||||
case 'LOBBY-ENABLED':
|
||||
notificationProps.descriptionKey = `lobby.notificationLobby${message.value ? 'En' : 'Dis'}abled`;
|
||||
break;
|
||||
case 'LOBBY-ACCESS-GRANTED':
|
||||
notificationProps.descriptionKey = 'lobby.notificationLobbyAccessGranted';
|
||||
notificationProps.descriptionArguments.targetParticipantName
|
||||
= getKnockingParticipantById(getState, message.value)?.name;
|
||||
break;
|
||||
case 'LOBBY-ACCESS-DENIED':
|
||||
notificationProps.descriptionKey = 'lobby.notificationLobbyAccessDenied';
|
||||
notificationProps.descriptionArguments.targetParticipantName
|
||||
= getKnockingParticipantById(getState, message.value)?.name;
|
||||
break;
|
||||
}
|
||||
|
||||
dispatch(showNotification(notificationProps, 5000));
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* @flow */
|
||||
|
||||
import { jitsiLocalStorage } from 'js-utils';
|
||||
import { jitsiLocalStorage } from '@jitsi/js-utils';
|
||||
|
||||
import logger from '../logger';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @flow
|
||||
|
||||
import { randomInt } from 'js-utils/random';
|
||||
import { randomInt } from '@jitsi/js-utils/random';
|
||||
import React, { Component } from 'react';
|
||||
import type { Dispatch } from 'redux';
|
||||
|
||||
|
||||
@@ -14,6 +14,11 @@ export const SET_DEVICE_STATUS = 'SET_DEVICE_STATUS';
|
||||
*/
|
||||
export const SET_SKIP_PREJOIN = 'SET_SKIP_PREJOIN';
|
||||
|
||||
/**
|
||||
* Action type used to set the mandatory stance of the prejoin display name.
|
||||
*/
|
||||
export const SET_PREJOIN_DISPLAY_NAME_REQUIRED = 'SET_PREJOIN_DISPLAY_NAME_REQUIRED';
|
||||
|
||||
/**
|
||||
* Action type to set the country to dial out to.
|
||||
*/
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
SET_DIALOUT_COUNTRY,
|
||||
SET_DIALOUT_NUMBER,
|
||||
SET_DIALOUT_STATUS,
|
||||
SET_PREJOIN_DISPLAY_NAME_REQUIRED,
|
||||
SET_SKIP_PREJOIN,
|
||||
SET_JOIN_BY_PHONE_DIALOG_VISIBLITY,
|
||||
SET_PREJOIN_DEVICE_ERRORS,
|
||||
@@ -198,14 +199,13 @@ export function initPrejoin(tracks: Object[], errors: Object) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins the conference.
|
||||
* Action used to start the conference.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function joinConference() {
|
||||
return function(dispatch: Function) {
|
||||
dispatch(setPrejoinPageVisibility(false));
|
||||
dispatch(startConference());
|
||||
return {
|
||||
type: PREJOIN_START_CONFERENCE
|
||||
};
|
||||
}
|
||||
|
||||
@@ -342,6 +342,17 @@ export function setDialOutCountry(value: Object) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action used to set the stance of the display name.
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function setPrejoinDisplayNameRequired() {
|
||||
return {
|
||||
type: SET_PREJOIN_DISPLAY_NAME_REQUIRED
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action used to set the dial out number.
|
||||
*
|
||||
@@ -406,14 +417,3 @@ export function setPrejoinPageVisibility(value: boolean) {
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action used to mark the start of the conference.
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
function startConference() {
|
||||
return {
|
||||
type: PREJOIN_START_CONFERENCE
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { getRoomName } from '../../base/conference';
|
||||
import { translate } from '../../base/i18n';
|
||||
import { Icon, IconPhone, IconVolumeOff } from '../../base/icons';
|
||||
import { isVideoMutedByUser } from '../../base/media';
|
||||
import { ActionButton, InputField, PreMeetingScreen } from '../../base/premeeting';
|
||||
import { ActionButton, InputField, PreMeetingScreen, ToggleButton } from '../../base/premeeting';
|
||||
import { connect } from '../../base/redux';
|
||||
import { getDisplayName, updateSettings } from '../../base/settings';
|
||||
import { getLocalJitsiVideoTrack } from '../../base/tracks';
|
||||
@@ -18,9 +18,11 @@ import {
|
||||
setJoinByPhoneDialogVisiblity as setJoinByPhoneDialogVisiblityAction
|
||||
} from '../actions';
|
||||
import {
|
||||
isJoinByPhoneButtonVisible,
|
||||
isDeviceStatusVisible,
|
||||
isJoinByPhoneDialogVisible
|
||||
isDisplayNameRequired,
|
||||
isJoinByPhoneButtonVisible,
|
||||
isJoinByPhoneDialogVisible,
|
||||
isPrejoinSkipped
|
||||
} from '../functions';
|
||||
|
||||
import JoinByPhoneDialog from './dialogs/JoinByPhoneDialog';
|
||||
@@ -28,6 +30,11 @@ import DeviceStatus from './preview/DeviceStatus';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Flag signaling if the 'skip prejoin' button is toggled or not.
|
||||
*/
|
||||
buttonIsToggled: boolean,
|
||||
|
||||
/**
|
||||
* Flag signaling if the device status is visible or not.
|
||||
*/
|
||||
@@ -38,6 +45,11 @@ type Props = {
|
||||
*/
|
||||
hasJoinByPhoneButton: boolean,
|
||||
|
||||
/**
|
||||
* If join button is disabled or not.
|
||||
*/
|
||||
joinButtonDisabled: boolean,
|
||||
|
||||
/**
|
||||
* Joins the current meeting.
|
||||
*/
|
||||
@@ -73,11 +85,21 @@ type Props = {
|
||||
*/
|
||||
setJoinByPhoneDialogVisiblity: Function,
|
||||
|
||||
/**
|
||||
* Indicates whether the avatar should be shown when video is off
|
||||
*/
|
||||
showAvatar: boolean,
|
||||
|
||||
/**
|
||||
* Flag signaling the visibility of camera preview.
|
||||
*/
|
||||
showCameraPreview: boolean,
|
||||
|
||||
/**
|
||||
* Flag signaling the visibility of join label, input and buttons
|
||||
*/
|
||||
showJoinActions: boolean,
|
||||
|
||||
/**
|
||||
* If 'JoinByPhoneDialog' is visible or not.
|
||||
*/
|
||||
@@ -106,6 +128,15 @@ type State = {
|
||||
* This component is displayed before joining a meeting.
|
||||
*/
|
||||
class Prejoin extends Component<Props, State> {
|
||||
/**
|
||||
* Default values for {@code Prejoin} component's properties.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static defaultProps = {
|
||||
showJoinActions: true
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes a new {@code Prejoin} instance.
|
||||
*
|
||||
@@ -120,22 +151,22 @@ class Prejoin extends Component<Props, State> {
|
||||
|
||||
this._closeDialog = this._closeDialog.bind(this);
|
||||
this._showDialog = this._showDialog.bind(this);
|
||||
this._onCheckboxChange = this._onCheckboxChange.bind(this);
|
||||
this._onToggleButtonClick = this._onToggleButtonClick.bind(this);
|
||||
this._onDropdownClose = this._onDropdownClose.bind(this);
|
||||
this._onOptionsClick = this._onOptionsClick.bind(this);
|
||||
this._setName = this._setName.bind(this);
|
||||
}
|
||||
|
||||
_onCheckboxChange: () => void;
|
||||
_onToggleButtonClick: () => void;
|
||||
|
||||
/**
|
||||
* Handler for the checkbox.
|
||||
* Handler for the toggle button.
|
||||
*
|
||||
* @param {Object} e - The synthetic event.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onCheckboxChange(e) {
|
||||
this.props.setSkipPrejoin(e.target.checked);
|
||||
_onToggleButtonClick() {
|
||||
this.props.setSkipPrejoin(!this.props.buttonIsToggled);
|
||||
}
|
||||
|
||||
_onDropdownClose: () => void;
|
||||
@@ -212,77 +243,78 @@ class Prejoin extends Component<Props, State> {
|
||||
*/
|
||||
render() {
|
||||
const {
|
||||
joinButtonDisabled,
|
||||
hasJoinByPhoneButton,
|
||||
joinConference,
|
||||
joinConferenceWithoutAudio,
|
||||
name,
|
||||
showAvatar,
|
||||
showCameraPreview,
|
||||
showDialog,
|
||||
showJoinActions,
|
||||
t,
|
||||
videoTrack
|
||||
} = this.props;
|
||||
|
||||
const { _closeDialog, _onCheckboxChange, _onDropdownClose, _onOptionsClick, _setName, _showDialog } = this;
|
||||
const { _closeDialog, _onDropdownClose, _onOptionsClick, _setName, _showDialog } = this;
|
||||
const { showJoinByPhoneButtons } = this.state;
|
||||
|
||||
return (
|
||||
<PreMeetingScreen
|
||||
footer = { this._renderFooter() }
|
||||
name = { name }
|
||||
showAvatar = { showAvatar }
|
||||
showConferenceInfo = { showJoinActions }
|
||||
skipPrejoinButton = { this._renderSkipPrejoinButton() }
|
||||
title = { t('prejoin.joinMeeting') }
|
||||
videoMuted = { !showCameraPreview }
|
||||
videoTrack = { videoTrack }>
|
||||
<div className = 'prejoin-input-area-container'>
|
||||
<div className = 'prejoin-input-area'>
|
||||
<InputField
|
||||
onChange = { _setName }
|
||||
onSubmit = { joinConference }
|
||||
placeHolder = { t('dialog.enterDisplayName') }
|
||||
value = { name } />
|
||||
{showJoinActions && (
|
||||
<div className = 'prejoin-input-area-container'>
|
||||
<div className = 'prejoin-input-area'>
|
||||
<InputField
|
||||
onChange = { _setName }
|
||||
onSubmit = { joinConference }
|
||||
placeHolder = { t('dialog.enterDisplayName') }
|
||||
value = { name } />
|
||||
|
||||
<div className = 'prejoin-preview-dropdown-container'>
|
||||
<InlineDialog
|
||||
content = { <div className = 'prejoin-preview-dropdown-btns'>
|
||||
<div
|
||||
className = 'prejoin-preview-dropdown-btn'
|
||||
onClick = { joinConferenceWithoutAudio }>
|
||||
<Icon
|
||||
className = 'prejoin-preview-dropdown-icon'
|
||||
size = { 24 }
|
||||
src = { IconVolumeOff } />
|
||||
{ t('prejoin.joinWithoutAudio') }
|
||||
</div>
|
||||
{hasJoinByPhoneButton && <div
|
||||
className = 'prejoin-preview-dropdown-btn'
|
||||
onClick = { _showDialog }>
|
||||
<Icon
|
||||
className = 'prejoin-preview-dropdown-icon'
|
||||
size = { 24 }
|
||||
src = { IconPhone } />
|
||||
{ t('prejoin.joinAudioByPhone') }
|
||||
</div>}
|
||||
</div> }
|
||||
isOpen = { showJoinByPhoneButtons }
|
||||
onClose = { _onDropdownClose }>
|
||||
<ActionButton
|
||||
disabled = { !name }
|
||||
hasOptions = { true }
|
||||
onClick = { joinConference }
|
||||
onOptionsClick = { _onOptionsClick }
|
||||
type = 'primary'>
|
||||
{ t('prejoin.joinMeeting') }
|
||||
</ActionButton>
|
||||
</InlineDialog>
|
||||
<div className = 'prejoin-preview-dropdown-container'>
|
||||
<InlineDialog
|
||||
content = { <div className = 'prejoin-preview-dropdown-btns'>
|
||||
<div
|
||||
className = 'prejoin-preview-dropdown-btn'
|
||||
onClick = { joinConferenceWithoutAudio }>
|
||||
<Icon
|
||||
className = 'prejoin-preview-dropdown-icon'
|
||||
size = { 24 }
|
||||
src = { IconVolumeOff } />
|
||||
{ t('prejoin.joinWithoutAudio') }
|
||||
</div>
|
||||
{hasJoinByPhoneButton && <div
|
||||
className = 'prejoin-preview-dropdown-btn'
|
||||
onClick = { _showDialog }>
|
||||
<Icon
|
||||
className = 'prejoin-preview-dropdown-icon'
|
||||
size = { 24 }
|
||||
src = { IconPhone } />
|
||||
{ t('prejoin.joinAudioByPhone') }
|
||||
</div>}
|
||||
</div> }
|
||||
isOpen = { showJoinByPhoneButtons }
|
||||
onClose = { _onDropdownClose }>
|
||||
<ActionButton
|
||||
disabled = { joinButtonDisabled }
|
||||
hasOptions = { true }
|
||||
onClick = { joinConference }
|
||||
onOptionsClick = { _onOptionsClick }
|
||||
type = 'primary'>
|
||||
{ t('prejoin.joinMeeting') }
|
||||
</ActionButton>
|
||||
</InlineDialog>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className = 'prejoin-checkbox-container'>
|
||||
<input
|
||||
className = 'prejoin-checkbox'
|
||||
onChange = { _onCheckboxChange }
|
||||
type = 'checkbox' />
|
||||
<span>{t('prejoin.doNotShow')}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{ showDialog && (
|
||||
<JoinByPhoneDialog
|
||||
joinConferenceWithoutAudio = { joinConferenceWithoutAudio }
|
||||
@@ -300,6 +332,25 @@ class Prejoin extends Component<Props, State> {
|
||||
_renderFooter() {
|
||||
return this.props.deviceStatusVisible && <DeviceStatus />;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the 'skip prejoin' button.
|
||||
*
|
||||
* @returns {React$Element}
|
||||
*/
|
||||
_renderSkipPrejoinButton() {
|
||||
const { buttonIsToggled, t } = this.props;
|
||||
|
||||
return (
|
||||
<div className = 'prejoin-checkbox-container'>
|
||||
<ToggleButton
|
||||
isToggled = { buttonIsToggled }
|
||||
onClick = { this._onToggleButtonClick }>
|
||||
{t('prejoin.doNotShow')}
|
||||
</ToggleButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -309,9 +360,14 @@ class Prejoin extends Component<Props, State> {
|
||||
* @returns {Object}
|
||||
*/
|
||||
function mapStateToProps(state): Object {
|
||||
const name = getDisplayName(state);
|
||||
const joinButtonDisabled = isDisplayNameRequired(state) && !name;
|
||||
|
||||
return {
|
||||
buttonIsToggled: isPrejoinSkipped(state),
|
||||
joinButtonDisabled,
|
||||
name,
|
||||
deviceStatusVisible: isDeviceStatusVisible(state),
|
||||
name: getDisplayName(state),
|
||||
roomName: getRoomName(state),
|
||||
showDialog: isJoinByPhoneDialogVisible(state),
|
||||
hasJoinByPhoneButton: isJoinByPhoneButtonVisible(state),
|
||||
|
||||
93
react/features/prejoin/components/PrejoinApp.js
Normal file
93
react/features/prejoin/components/PrejoinApp.js
Normal file
@@ -0,0 +1,93 @@
|
||||
// @flow
|
||||
|
||||
import { AtlasKitThemeProvider } from '@atlaskit/theme';
|
||||
import React from 'react';
|
||||
|
||||
import { BaseApp } from '../../../features/base/app';
|
||||
import { setConfig } from '../../base/config';
|
||||
import { createPrejoinTracks } from '../../base/tracks';
|
||||
import { initPrejoin } from '../actions';
|
||||
|
||||
import Prejoin from './Prejoin';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Indicates whether the avatar should be shown when video is off
|
||||
*/
|
||||
showAvatar: boolean,
|
||||
|
||||
/**
|
||||
* Flag signaling the visibility of join label, input and buttons
|
||||
*/
|
||||
showJoinActions: boolean,
|
||||
};
|
||||
|
||||
/**
|
||||
* Wrapper application for prejoin.
|
||||
*
|
||||
* @extends BaseApp
|
||||
*/
|
||||
export default class PrejoinApp extends BaseApp<Props> {
|
||||
_init: Promise<*>;
|
||||
|
||||
/**
|
||||
* Navigates to {@link Prejoin} upon mount.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
componentDidMount() {
|
||||
super.componentDidMount();
|
||||
|
||||
this._init.then(async () => {
|
||||
const { store } = this.state;
|
||||
const { dispatch } = store;
|
||||
const { showAvatar, showJoinActions } = this.props;
|
||||
|
||||
super._navigate({
|
||||
component: Prejoin,
|
||||
props: {
|
||||
showAvatar,
|
||||
showJoinActions
|
||||
}
|
||||
});
|
||||
|
||||
const { startWithAudioMuted, startWithVideoMuted } = store.getState()['features/base/settings'];
|
||||
|
||||
dispatch(setConfig({
|
||||
prejoinPageEnabled: true,
|
||||
startWithAudioMuted,
|
||||
startWithVideoMuted
|
||||
}));
|
||||
|
||||
const { tryCreateLocalTracks, errors } = createPrejoinTracks();
|
||||
|
||||
const tracks = await tryCreateLocalTracks;
|
||||
|
||||
dispatch(initPrejoin(tracks, errors));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the parent method to inject {@link AtlasKitThemeProvider} as
|
||||
* the top most component.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
_createMainElement(component, props) {
|
||||
return (
|
||||
<AtlasKitThemeProvider mode = 'dark'>
|
||||
{ super._createMainElement(component, props) }
|
||||
</AtlasKitThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the platform specific dialog container.
|
||||
*
|
||||
* @returns {React$Element}
|
||||
*/
|
||||
_renderDialogContainer() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,27 @@ export function isDeviceStatusVisible(state: Object): boolean {
|
||||
&& !state['features/base/config'].startSilent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector for determining if the display name is mandatory.
|
||||
*
|
||||
* @param {Object} state - The state of the app.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isDisplayNameRequired(state: Object): boolean {
|
||||
return state['features/prejoin'].isDisplayNameRequired
|
||||
|| state['features/base/config'].requireDisplayName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector for determining if the user has chosen to skip prejoin page.
|
||||
*
|
||||
* @param {Object} state - The state of the app.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isPrejoinSkipped(state: Object) {
|
||||
return state['features/prejoin'].userSelectedSkipPrejoin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the text for the prejoin status bar.
|
||||
*
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
// @flow
|
||||
|
||||
import { SET_AUDIO_MUTED, SET_VIDEO_MUTED } from '../base/media';
|
||||
import { MiddlewareRegistry } from '../base/redux';
|
||||
import { updateSettings } from '../base/settings';
|
||||
import { getLocalVideoTrack, replaceLocalTrack } from '../base/tracks';
|
||||
|
||||
import { PREJOIN_START_CONFERENCE } from './actionTypes';
|
||||
import { setPrejoinPageVisibility } from './actions';
|
||||
import { isPrejoinPageVisible } from './functions';
|
||||
|
||||
declare var APP: Object;
|
||||
|
||||
@@ -19,18 +23,43 @@ MiddlewareRegistry.register(store => next => async action => {
|
||||
const { getState, dispatch } = store;
|
||||
const state = getState();
|
||||
const { userSelectedSkipPrejoin } = state['features/prejoin'];
|
||||
const tracks = state['features/base/tracks'];
|
||||
const localVideoTrack = getLocalVideoTrack(state['features/base/tracks']);
|
||||
|
||||
userSelectedSkipPrejoin && dispatch(updateSettings({
|
||||
userSelectedSkipPrejoin
|
||||
}));
|
||||
|
||||
APP.conference.prejoinStart(tracks.map(t => t.jitsiTrack));
|
||||
if (localVideoTrack?.muted) {
|
||||
await dispatch(replaceLocalTrack(localVideoTrack.jitsiTrack, null));
|
||||
}
|
||||
|
||||
const jitsiTracks = getState()['features/base/tracks'].map(t => t.jitsiTrack);
|
||||
|
||||
dispatch(setPrejoinPageVisibility(false));
|
||||
APP.conference.prejoinStart(jitsiTracks);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case SET_AUDIO_MUTED: {
|
||||
if (isPrejoinPageVisible(store.getState())) {
|
||||
store.dispatch(updateSettings({
|
||||
startWithAudioMuted: Boolean(action.muted)
|
||||
}));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case SET_VIDEO_MUTED: {
|
||||
if (isPrejoinPageVisible(store.getState())) {
|
||||
store.dispatch(updateSettings({
|
||||
startWithVideoMuted: Boolean(action.muted)
|
||||
}));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return next(action);
|
||||
});
|
||||
|
||||
@@ -2,13 +2,14 @@ import { ReducerRegistry } from '../base/redux';
|
||||
|
||||
import {
|
||||
SET_DEVICE_STATUS,
|
||||
SET_DIALOUT_NUMBER,
|
||||
SET_DIALOUT_COUNTRY,
|
||||
SET_DIALOUT_NUMBER,
|
||||
SET_DIALOUT_STATUS,
|
||||
SET_JOIN_BY_PHONE_DIALOG_VISIBLITY,
|
||||
SET_SKIP_PREJOIN,
|
||||
SET_PREJOIN_DEVICE_ERRORS,
|
||||
SET_PREJOIN_PAGE_VISIBILITY
|
||||
SET_PREJOIN_DISPLAY_NAME_REQUIRED,
|
||||
SET_PREJOIN_PAGE_VISIBILITY,
|
||||
SET_SKIP_PREJOIN
|
||||
} from './actionTypes';
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
@@ -22,6 +23,7 @@ const DEFAULT_STATE = {
|
||||
},
|
||||
dialOutNumber: '',
|
||||
dialOutStatus: 'prejoin.dialing',
|
||||
isDisplayNameRequired: false,
|
||||
name: '',
|
||||
rawError: '',
|
||||
showPrejoin: true,
|
||||
@@ -94,6 +96,13 @@ ReducerRegistry.register(
|
||||
};
|
||||
}
|
||||
|
||||
case SET_PREJOIN_DISPLAY_NAME_REQUIRED: {
|
||||
return {
|
||||
...state,
|
||||
isDisplayNameRequired: true
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @flow
|
||||
|
||||
import { jitsiLocalStorage } from 'js-utils';
|
||||
import { jitsiLocalStorage } from '@jitsi/js-utils';
|
||||
|
||||
import { APP_WILL_MOUNT } from '../base/app';
|
||||
import { getURLWithoutParamsNormalized } from '../base/connection';
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
// @flow
|
||||
|
||||
import { openDialog } from '../../base/dialog';
|
||||
import { IconCrown } from '../../base/icons';
|
||||
import {
|
||||
getParticipantById,
|
||||
isLocalParticipantModerator,
|
||||
isParticipantModerator
|
||||
} from '../../base/participants';
|
||||
import { AbstractButton } from '../../base/toolbox';
|
||||
import type { AbstractButtonProps } from '../../base/toolbox';
|
||||
|
||||
import { GrantModeratorDialog } from '.';
|
||||
|
||||
export type Props = AbstractButtonProps & {
|
||||
|
||||
/**
|
||||
* The redux {@code dispatch} function.
|
||||
*/
|
||||
dispatch: Function,
|
||||
|
||||
/**
|
||||
* The ID of the participant for whom to grant moderator status.
|
||||
*/
|
||||
participantID: string,
|
||||
|
||||
/**
|
||||
* The function to be used to translate i18n labels.
|
||||
*/
|
||||
t: Function
|
||||
};
|
||||
|
||||
/**
|
||||
* An abstract remote video menu button which kicks the remote participant.
|
||||
*/
|
||||
export default class AbstractGrantModeratorButton extends AbstractButton<Props, *> {
|
||||
accessibilityLabel = 'toolbar.accessibilityLabel.grantModerator';
|
||||
icon = IconCrown;
|
||||
label = 'videothumbnail.grantModerator';
|
||||
|
||||
/**
|
||||
* Handles clicking / pressing the button, and kicks the participant.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_handleClick() {
|
||||
const { dispatch, participantID } = this.props;
|
||||
|
||||
dispatch(openDialog(GrantModeratorDialog, { participantID }));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function that maps parts of Redux state tree into component props.
|
||||
*
|
||||
* @param {Object} state - Redux state.
|
||||
* @param {Object} ownProps - Properties of component.
|
||||
* @private
|
||||
* @returns {{
|
||||
* visible: boolean
|
||||
* }}
|
||||
*/
|
||||
export function _mapStateToProps(state: Object, ownProps: Props) {
|
||||
const { participantID } = ownProps;
|
||||
|
||||
return {
|
||||
visible: isLocalParticipantModerator(state) && !isParticipantModerator(getParticipantById(state, participantID))
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// @flow
|
||||
|
||||
import { Component } from 'react';
|
||||
|
||||
import {
|
||||
createRemoteVideoMenuButtonEvent,
|
||||
sendAnalytics
|
||||
} from '../../analytics';
|
||||
import { grantModerator } from '../../base/participants';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The Redux dispatch function.
|
||||
*/
|
||||
dispatch: Function,
|
||||
|
||||
/**
|
||||
* The ID of the remote participant to be granted moderator rights.
|
||||
*/
|
||||
participantID: string,
|
||||
|
||||
/**
|
||||
* Function to translate i18n labels.
|
||||
*/
|
||||
t: Function
|
||||
};
|
||||
|
||||
/**
|
||||
* Abstract dialog to confirm granting moderator to a participant.
|
||||
*/
|
||||
export default class AbstractGrantModeratorDialog
|
||||
extends Component<Props> {
|
||||
/**
|
||||
* Initializes a new {@code AbstractGrantModeratorDialog} instance.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this._onSubmit = this._onSubmit.bind(this);
|
||||
}
|
||||
|
||||
_onSubmit: () => boolean;
|
||||
|
||||
/**
|
||||
* Callback for the confirm button.
|
||||
*
|
||||
* @private
|
||||
* @returns {boolean} - True (to note that the modal should be closed).
|
||||
*/
|
||||
_onSubmit() {
|
||||
const { dispatch, participantID } = this.props;
|
||||
|
||||
sendAnalytics(createRemoteVideoMenuButtonEvent(
|
||||
'grant.moderator.button',
|
||||
{
|
||||
'participant_id': participantID
|
||||
}));
|
||||
|
||||
dispatch(grantModerator(participantID));
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// @flow
|
||||
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { connect } from '../../../base/redux';
|
||||
import AbstractGrantModeratorButton, {
|
||||
_mapStateToProps
|
||||
} from '../AbstractGrantModeratorButton';
|
||||
|
||||
export default translate(connect(_mapStateToProps)(AbstractGrantModeratorButton));
|
||||
@@ -0,0 +1,32 @@
|
||||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { ConfirmDialog } from '../../../base/dialog';
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { connect } from '../../../base/redux';
|
||||
import AbstractGrantModeratorDialog
|
||||
from '../AbstractGrantModeratorDialog';
|
||||
|
||||
/**
|
||||
* Dialog to confirm a remote participant kick action.
|
||||
*/
|
||||
class GrantModeratorDialog extends AbstractGrantModeratorDialog {
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
return (
|
||||
<ConfirmDialog
|
||||
contentKey = 'dialog.grantModeratorDialog'
|
||||
onSubmit = { this._onSubmit } />
|
||||
);
|
||||
}
|
||||
|
||||
_onSubmit: () => boolean;
|
||||
}
|
||||
|
||||
export default translate(connect()(GrantModeratorDialog));
|
||||
@@ -12,6 +12,7 @@ import { StyleType } from '../../../base/styles';
|
||||
import { PrivateMessageButton } from '../../../chat';
|
||||
import { hideRemoteVideoMenu } from '../../actions';
|
||||
|
||||
import GrantModeratorButton from './GrantModeratorButton';
|
||||
import KickButton from './KickButton';
|
||||
import MuteButton from './MuteButton';
|
||||
import PinButton from './PinButton';
|
||||
@@ -98,6 +99,8 @@ class RemoteVideoMenu extends Component<Props> {
|
||||
buttons.push(<MuteButton { ...buttonProps } />);
|
||||
}
|
||||
|
||||
buttons.push(<GrantModeratorButton { ...buttonProps } />);
|
||||
|
||||
if (!_disableKick) {
|
||||
buttons.push(<KickButton { ...buttonProps } />);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
// @flow
|
||||
|
||||
export {
|
||||
default as GrantModeratorDialog
|
||||
} from './GrantModeratorDialog';
|
||||
export {
|
||||
default as KickRemoteParticipantDialog
|
||||
} from './KickRemoteParticipantDialog';
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/* @flow */
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { IconCrown } from '../../../base/icons';
|
||||
import { connect } from '../../../base/redux';
|
||||
import AbstractGrantModeratorButton, {
|
||||
_mapStateToProps,
|
||||
type Props
|
||||
} from '../AbstractGrantModeratorButton';
|
||||
|
||||
import RemoteVideoMenuButton from './RemoteVideoMenuButton';
|
||||
|
||||
declare var interfaceConfig: Object;
|
||||
|
||||
/**
|
||||
* Implements a React {@link Component} which displays a button for granting
|
||||
* moderator to a participant.
|
||||
*/
|
||||
class GrantModeratorButton extends AbstractGrantModeratorButton {
|
||||
/**
|
||||
* Instantiates a new {@code GrantModeratorButton}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this._handleClick = this._handleClick.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { participantID, t, visible } = this.props;
|
||||
|
||||
if (!visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<RemoteVideoMenuButton
|
||||
buttonText = { t('videothumbnail.grantModerator') }
|
||||
displayClass = 'grantmoderatorlink'
|
||||
icon = { IconCrown }
|
||||
id = { `grantmoderatorlink_${participantID}` }
|
||||
// eslint-disable-next-line react/jsx-handler-names
|
||||
onClick = { this._handleClick } />
|
||||
);
|
||||
}
|
||||
|
||||
_handleClick: () => void
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(GrantModeratorButton));
|
||||
@@ -0,0 +1,38 @@
|
||||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { Dialog } from '../../../base/dialog';
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { connect } from '../../../base/redux';
|
||||
import AbstractGrantModeratorDialog
|
||||
from '../AbstractGrantModeratorDialog';
|
||||
|
||||
/**
|
||||
* Dialog to confirm a grant moderator action.
|
||||
*/
|
||||
class GrantModeratorDialog extends AbstractGrantModeratorDialog {
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
return (
|
||||
<Dialog
|
||||
okKey = 'dialog.Yes'
|
||||
onSubmit = { this._onSubmit }
|
||||
titleKey = 'dialog.grantModeratorTitle'
|
||||
width = 'small'>
|
||||
<div>
|
||||
{ this.props.t('dialog.grantModeratorDialog') }
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
_onSubmit: () => boolean;
|
||||
}
|
||||
|
||||
export default translate(connect()(GrantModeratorDialog));
|
||||
@@ -8,6 +8,7 @@ import { Popover } from '../../../base/popover';
|
||||
import { connect } from '../../../base/redux';
|
||||
|
||||
import {
|
||||
GrantModeratorButton,
|
||||
MuteButton,
|
||||
MuteEveryoneElseButton,
|
||||
KickButton,
|
||||
@@ -195,6 +196,12 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
|
||||
);
|
||||
}
|
||||
|
||||
buttons.push(
|
||||
<GrantModeratorButton
|
||||
key = 'grant-moderator'
|
||||
participantID = { participantID } />
|
||||
);
|
||||
|
||||
if (!_disableKick) {
|
||||
buttons.push(
|
||||
<KickButton
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
// @flow
|
||||
|
||||
export { default as GrantModeratorButton } from './GrantModeratorButton';
|
||||
export {
|
||||
default as GrantModeratorDialog
|
||||
} from './GrantModeratorDialog';
|
||||
export { default as KickButton } from './KickButton';
|
||||
export {
|
||||
default as KickRemoteParticipantDialog
|
||||
|
||||
111
react/features/rtcstats/RTCStats.js
Normal file
111
react/features/rtcstats/RTCStats.js
Normal file
@@ -0,0 +1,111 @@
|
||||
import rtcstatsInit from 'rtcstats/rtcstats';
|
||||
import traceInit from 'rtcstats/trace-ws';
|
||||
|
||||
import {
|
||||
createRTCStatsTraceCloseEvent,
|
||||
sendAnalytics
|
||||
} from '../analytics';
|
||||
|
||||
import logger from './logger';
|
||||
|
||||
/**
|
||||
* Filter out RTCPeerConnection that are created by callstats.io.
|
||||
*
|
||||
* @param {*} config - Config object sent to the PC c'tor.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function connectionFilter(config) {
|
||||
if (config && config.iceServers[0] && config.iceServers[0].urls) {
|
||||
for (const iceUrl of config.iceServers[0].urls) {
|
||||
if (iceUrl.indexOf('taas.callstats.io') >= 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class that controls the rtcstats flow, because it overwrites and proxies global function it should only be
|
||||
* initialized once.
|
||||
*/
|
||||
class RTCStats {
|
||||
/**
|
||||
* Initialize the rtcstats components. First off we initialize the trace, which is a wrapped websocket
|
||||
* that does the actual communication with the server. Secondly, the rtcstats component is initialized,
|
||||
* it overwrites GUM and PeerConnection global functions and adds a proxy over them used to capture stats.
|
||||
* Note, lib-jitsi-meet takes references to these methods before initializing so the init method needs to be
|
||||
* loaded before it does.
|
||||
*
|
||||
* @param {Object} options -.
|
||||
* @param {string} options.rtcstatsEndpoint - The Amplitude app key required.
|
||||
* @param {number} options.rtcstatsPollInterval - The getstats poll interval in ms.
|
||||
* @returns {void}
|
||||
*/
|
||||
init(options) {
|
||||
this.handleTraceWSClose = this.handleTraceWSClose.bind(this);
|
||||
this.trace = traceInit(options.rtcstatsEndpoint, this.handleTraceWSClose);
|
||||
rtcstatsInit(this.trace, options.rtcstatsPollInterval, [ '' ], connectionFilter);
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether or not the RTCStats is initialized.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isInitialized() {
|
||||
return this.initialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send identity data to rtcstats server, this will be reflected in the identity section of the stats dump.
|
||||
* It can be generally used to send additional metadata that might be relevant such as amplitude user data
|
||||
* or deployment specific information.
|
||||
*
|
||||
* @param {Object} identityData - Metadata object to send as identity.
|
||||
* @returns {void}
|
||||
*/
|
||||
sendIdentityData(identityData) {
|
||||
this.trace && this.trace('identity', null, identityData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to the rtcstats server instance. Stats (data obtained from getstats) won't be send until the
|
||||
* connect successfully initializes, however calls to GUM are recorded in an internal buffer even if not
|
||||
* connected and sent once it is established.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
connect() {
|
||||
this.trace && this.trace.connect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Self explanatory; closes the web socked connection.
|
||||
* Note, at the point of writing this documentation there was no method to reset the function overwrites,
|
||||
* thus even if the websocket is closed the global function proxies are still active but send no data,
|
||||
* this shouldn't influence the normal flow of the application.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
close() {
|
||||
this.trace && this.trace.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* The way rtcstats is currently designed the ws wouldn't normally be closed by the application logic but rather
|
||||
* by the page being closed/reloaded. Using this assumption any onclose event is most likely something abnormal
|
||||
* that happened on the ws. We then track this in order to determine how many rtcstats connection were closed
|
||||
* prematurely.
|
||||
*
|
||||
* @param {Object} closeEvent - Event sent by ws onclose.
|
||||
* @returns {void}
|
||||
*/
|
||||
handleTraceWSClose(closeEvent) {
|
||||
logger.info('RTCStats trace ws closed', closeEvent);
|
||||
|
||||
sendAnalytics(createRTCStatsTraceCloseEvent(closeEvent));
|
||||
}
|
||||
}
|
||||
|
||||
export default new RTCStats();
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user