mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-01-28 01:20:20 +00:00
Compare commits
1 Commits
4385
...
jibri-queu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7cb719be0 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -84,4 +84,3 @@ android/app/google-services.json
|
||||
ios/app/dropbox.key
|
||||
ios/app/GoogleService-Info.plist
|
||||
|
||||
.vscode
|
||||
|
||||
2
Makefile
2
Makefile
@@ -51,8 +51,6 @@ deploy-appbundle:
|
||||
$(BUILD_DIR)/video-blur-effect.min.map \
|
||||
$(BUILD_DIR)/rnnoise-processor.min.js \
|
||||
$(BUILD_DIR)/rnnoise-processor.min.map \
|
||||
$(BUILD_DIR)/close3.min.js \
|
||||
$(BUILD_DIR)/close3.min.map \
|
||||
$(DEPLOY_DIR)
|
||||
|
||||
deploy-lib-jitsi-meet:
|
||||
|
||||
@@ -121,8 +121,7 @@ import { suspendDetected } from './react/features/power-monitor';
|
||||
import {
|
||||
initPrejoin,
|
||||
isPrejoinPageEnabled,
|
||||
isPrejoinPageVisible,
|
||||
makePrecallTest
|
||||
isPrejoinPageVisible
|
||||
} from './react/features/prejoin';
|
||||
import { createRnnoiseProcessorPromise } from './react/features/rnnoise';
|
||||
import { toggleScreenshotCaptureEffect } from './react/features/screenshot-capture';
|
||||
@@ -760,15 +759,7 @@ export default {
|
||||
}
|
||||
|
||||
if (isPrejoinPageEnabled(APP.store.getState())) {
|
||||
_connectionPromise = connect(roomName).then(c => {
|
||||
// we want to initialize it early, in case of errors to be able
|
||||
// to gather logs
|
||||
APP.connection = c;
|
||||
|
||||
return c;
|
||||
});
|
||||
|
||||
APP.store.dispatch(makePrecallTest(this._getConferenceOptions()));
|
||||
_connectionPromise = connect(roomName);
|
||||
|
||||
const { tryCreateLocalTracks, errors } = this.createInitialLocalTracks(initialOptions);
|
||||
const tracks = await tryCreateLocalTracks;
|
||||
@@ -1215,6 +1206,10 @@ export default {
|
||||
|
||||
// end used by torture
|
||||
|
||||
getLogs() {
|
||||
return room.getLogs();
|
||||
},
|
||||
|
||||
/**
|
||||
* Download logs, a function that can be called from console while
|
||||
* debugging.
|
||||
@@ -1223,7 +1218,7 @@ export default {
|
||||
saveLogs(filename = 'meetlog.json') {
|
||||
// this can be called from console and will not have reference to this
|
||||
// that's why we reference the global var
|
||||
const logs = APP.connection.getLogs();
|
||||
const logs = APP.conference.getLogs();
|
||||
const data = encodeURIComponent(JSON.stringify(logs, null, ' '));
|
||||
|
||||
const elem = document.createElement('a');
|
||||
@@ -2861,14 +2856,7 @@ export default {
|
||||
this._room = undefined;
|
||||
room = undefined;
|
||||
|
||||
/**
|
||||
* Don't call {@code notifyReadyToClose} if the promotional page flag is set
|
||||
* and let the page take care of sending the message, since there will be
|
||||
* a redirect to the page regardlessly.
|
||||
*/
|
||||
if (!interfaceConfig.SHOW_PROMOTIONAL_CLOSE_PAGE) {
|
||||
APP.API.notifyReadyToClose();
|
||||
}
|
||||
APP.API.notifyReadyToClose();
|
||||
APP.store.dispatch(maybeRedirectToWelcomePage(values[0]));
|
||||
});
|
||||
},
|
||||
|
||||
62
config.js
62
config.js
@@ -118,9 +118,6 @@ var config = {
|
||||
// Valid values are in the range 6000 to 510000
|
||||
// opusMaxAverageBitrate: 20000,
|
||||
|
||||
// Enables redundancy for Opus
|
||||
// enableOpusRed: false
|
||||
|
||||
// Video
|
||||
|
||||
// Sets the preferred resolution (height) for local video. Defaults to 720.
|
||||
@@ -164,7 +161,6 @@ var config = {
|
||||
// Note that it's not recommended to do this because simulcast is not
|
||||
// supported when using H.264. For 1-to-1 calls this setting is enabled by
|
||||
// default and can be toggled in the p2p section.
|
||||
// This option has been deprecated, use preferredCodec under videoQuality section instead.
|
||||
// preferH264: true,
|
||||
|
||||
// If set to true, disable H.264 video codec by stripping it out of the
|
||||
@@ -238,18 +234,6 @@ var config = {
|
||||
|
||||
// Specify the settings for video quality optimizations on the client.
|
||||
// videoQuality: {
|
||||
// // Provides a way to prevent a video codec from being negotiated on the JVB connection. The codec specified
|
||||
// // here will be removed from the list of codecs present in the SDP answer generated by the client. If the
|
||||
// // same codec is specified for both the disabled and preferred option, the disable settings will prevail.
|
||||
// // Note that 'VP8' cannot be disabled since it's a mandatory codec, the setting will be ignored in this case.
|
||||
// disabledCodec: 'H264',
|
||||
//
|
||||
// // Provides a way to set a preferred video codec for the JVB connection. If 'H264' is specified here,
|
||||
// // simulcast will be automatically disabled since JVB doesn't support H264 simulcast yet. This will only
|
||||
// // rearrange the the preference order of the codecs in the SDP answer generated by the browser only if the
|
||||
// // preferred codec specified here is present. Please ensure that the JVB offers the specified codec for this
|
||||
// // to take effect.
|
||||
// preferredCodec: 'VP8',
|
||||
//
|
||||
// // Provides a way to configure the maximum bitrates that will be enforced on the simulcast streams for
|
||||
// // video tracks. The keys in the object represent the type of the stream (LD, SD or HD) and the values
|
||||
@@ -260,21 +244,6 @@ var config = {
|
||||
// low: 200000,
|
||||
// standard: 500000,
|
||||
// high: 1500000
|
||||
// },
|
||||
//
|
||||
// // The options can be used to override default thresholds of video thumbnail heights corresponding to
|
||||
// // the video quality levels used in the application. At the time of this writing the allowed levels are:
|
||||
// // 'low' - for the low quality level (180p at the time of this writing)
|
||||
// // 'standard' - for the medium quality level (360p)
|
||||
// // 'high' - for the high quality level (720p)
|
||||
// // The keys should be positive numbers which represent the minimal thumbnail height for the quality level.
|
||||
// //
|
||||
// // With the default config value below the application will use 'low' quality until the thumbnails are
|
||||
// // at least 360 pixels tall. If the thumbnail height reaches 720 pixels then the application will switch to
|
||||
// // the high quality.
|
||||
// minHeightForQualityLvl: {
|
||||
// 360: 'standard,
|
||||
// 720: 'high'
|
||||
// }
|
||||
// },
|
||||
|
||||
@@ -340,9 +309,6 @@ var config = {
|
||||
// UI
|
||||
//
|
||||
|
||||
// Hides lobby button
|
||||
// hideLobbyButton: false,
|
||||
|
||||
// Require users to always specify a display name.
|
||||
// requireDisplayName: true,
|
||||
|
||||
@@ -391,10 +357,6 @@ var config = {
|
||||
// set or the lobby is not enabled.
|
||||
// enableInsecureRoomNameWarning: false,
|
||||
|
||||
// Whether to automatically copy invitation URL after creating a room.
|
||||
// Document should be focused for this option to work
|
||||
// enableAutomaticUrlCopy: false,
|
||||
|
||||
// Stats
|
||||
//
|
||||
|
||||
@@ -458,20 +420,13 @@ var config = {
|
||||
// iceTransportPolicy: 'all',
|
||||
|
||||
// If set to true, it will prefer to use H.264 for P2P calls (if H.264
|
||||
// is supported). This setting is deprecated, use preferredCodec instead.
|
||||
// is supported).
|
||||
// preferH264: true
|
||||
|
||||
// Provides a way to set the video codec preference on the p2p connection. Acceptable
|
||||
// codec values are 'VP8', 'VP9' and 'H264'.
|
||||
// preferredCodec: 'H264',
|
||||
|
||||
// If set to true, disable H.264 video codec by stripping it out of the
|
||||
// SDP. This setting is deprecated, use disabledCodec instead.
|
||||
// SDP.
|
||||
// disableH264: false,
|
||||
|
||||
// Provides a way to prevent a video codec from being negotiated on the p2p connection.
|
||||
// disabledCodec: '',
|
||||
|
||||
// How long we're going to wait, before going back to P2P after the 3rd
|
||||
// participant has left the conference (to filter out page reload).
|
||||
// backToP2PDelay: 5
|
||||
@@ -489,12 +444,6 @@ var config = {
|
||||
// amplitudeAPPKey: '<APP_KEY>'
|
||||
|
||||
// Configuration for the rtcstats server:
|
||||
// By enabling rtcstats server every time a conference is joined the rtcstats
|
||||
// module connects to the provided rtcstatsEndpoint and sends statistics regarding
|
||||
// PeerConnection states along with getStats metrics polled at the specified
|
||||
// interval.
|
||||
// rtcstatsEnabled: true,
|
||||
|
||||
// In order to enable rtcstats one needs to provide a endpoint url.
|
||||
// rtcstatsEndpoint: wss://rtcstats-server-pilot.jitsi.net/,
|
||||
|
||||
@@ -656,13 +605,6 @@ var config = {
|
||||
tokenAuthUrl
|
||||
*/
|
||||
|
||||
/**
|
||||
* This property can be used to alter the generated meeting invite links (in combination with a branding domain
|
||||
* which is retrieved internally by jitsi meet) (e.g. https://meet.jit.si/someMeeting
|
||||
* can become https://brandedDomain/roomAlias)
|
||||
*/
|
||||
// brandingRoomAlias: null,
|
||||
|
||||
// List of undocumented settings used in lib-jitsi-meet
|
||||
/**
|
||||
_peerConnStatusOutOfLastNTimeout
|
||||
|
||||
@@ -82,7 +82,7 @@ function checkForAttachParametersAndConnect(id, password, connection) {
|
||||
*/
|
||||
function connect(id, password, roomName) {
|
||||
const connectionConfig = Object.assign({}, config);
|
||||
const { jwt } = APP.store.getState()['features/base/jwt'];
|
||||
const { issuer, jwt } = APP.store.getState()['features/base/jwt'];
|
||||
|
||||
// Use Websocket URL for the web app if configured. Note that there is no 'isWeb' check, because there's assumption
|
||||
// that this code executes only on web browsers/electron. This needs to be changed when mobile and web are unified.
|
||||
@@ -94,7 +94,11 @@ function connect(id, password, roomName) {
|
||||
// in future). It's included for the time being for Jitsi Meet and lib-jitsi-meet versions interoperability.
|
||||
connectionConfig.serviceUrl = connectionConfig.bosh = serviceUrl;
|
||||
|
||||
const connection = new JitsiMeetJS.JitsiConnection(null, jwt, connectionConfig);
|
||||
const connection
|
||||
= new JitsiMeetJS.JitsiConnection(
|
||||
null,
|
||||
jwt && issuer && issuer !== 'anonymous' ? jwt : undefined,
|
||||
connectionConfig);
|
||||
|
||||
if (config.iAmRecorder) {
|
||||
connection.addFeature(DISCO_JIBRI_FEATURE);
|
||||
@@ -207,9 +211,10 @@ export function openConnection({ id, password, retry, roomName }) {
|
||||
|
||||
return connect(id, password, roomName).catch(err => {
|
||||
if (retry) {
|
||||
const { jwt } = APP.store.getState()['features/base/jwt'];
|
||||
const { issuer, jwt } = APP.store.getState()['features/base/jwt'];
|
||||
|
||||
if (err === JitsiConnectionErrors.PASSWORD_REQUIRED && !jwt) {
|
||||
if (err === JitsiConnectionErrors.PASSWORD_REQUIRED
|
||||
&& (!jwt || issuer === 'anonymous')) {
|
||||
return AuthHandler.requestAuth(roomName, connect);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
.con-status {
|
||||
position: absolute;
|
||||
top: 40px;
|
||||
width: 100%;
|
||||
z-index: $toolbarZ + 3;
|
||||
|
||||
&-container {
|
||||
background: rgba(28, 32, 37, .5);
|
||||
border-radius: 3px;
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
margin: 0 auto;
|
||||
width: 304px;
|
||||
}
|
||||
|
||||
&-header {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
&-circle {
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
&--good {
|
||||
background: #31B76A;
|
||||
}
|
||||
|
||||
&--poor {
|
||||
background: #E12D2D;
|
||||
}
|
||||
|
||||
&--non-optimal {
|
||||
background: #E39623;
|
||||
}
|
||||
|
||||
&-arrow {
|
||||
&--up {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
&>svg {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
&-text {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&-details {
|
||||
border-top: 1px solid #5E6D7A;
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
74
css/_notifications.scss
Normal file
74
css/_notifications.scss
Normal file
@@ -0,0 +1,74 @@
|
||||
@include keyframes(exiting) {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translate(-200px, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.notificationsContainer {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
flex-direction: column;
|
||||
width: 400px;
|
||||
left: 80px;
|
||||
|
||||
.topContainer {
|
||||
padding-bottom: 16px;
|
||||
// transition: height 0.4s ease-in-out;
|
||||
|
||||
.notification {
|
||||
width: 400px;
|
||||
z-index: 5;
|
||||
|
||||
&:nth-child(n+2) {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
&.exiting {
|
||||
animation-name: exiting;
|
||||
animation-duration: 0.4s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bottomContainer {
|
||||
margin-bottom: 64px;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
|
||||
.notification {
|
||||
width: 400px;
|
||||
bottom: 0px;
|
||||
|
||||
&:nth-child(1) {
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
&:nth-child(n+2) {
|
||||
transition: transform 0.4s ease-in-out;
|
||||
z-index: 4;
|
||||
position: absolute;
|
||||
transform: translateY(100%) translateY(16px);
|
||||
}
|
||||
|
||||
|
||||
&:nth-child(n+4) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.exiting {
|
||||
animation-name: exiting;
|
||||
animation-duration: 0.4s;
|
||||
}
|
||||
|
||||
&.exiting+.notification {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,16 +39,6 @@
|
||||
margin-bottom: 14px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&-error {
|
||||
color: white;
|
||||
background-color: rgba(229, 75, 75, 0.5);
|
||||
width: 100%;
|
||||
padding: 3px;
|
||||
margin-top: 4px;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin name-placeholder {
|
||||
|
||||
@@ -197,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: 0 auto;
|
||||
}
|
||||
|
||||
video {
|
||||
|
||||
@@ -115,3 +115,19 @@
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.jibri-queue-info {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
|
||||
.footer {
|
||||
background: #a4b8a4D1;
|
||||
border-radius: 3px;
|
||||
font-weight: bold;
|
||||
color: #5e6d7a;
|
||||
padding: 5px;
|
||||
margin-right: 30px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -102,6 +102,6 @@ $flagsImagePath: "../images/";
|
||||
@import 'premeeting-screens';
|
||||
@import 'e2ee';
|
||||
@import 'responsive';
|
||||
@import 'connection-status';
|
||||
@import 'notifications';
|
||||
|
||||
/* Modules END */
|
||||
|
||||
2
debian/control
vendored
2
debian/control
vendored
@@ -47,7 +47,7 @@ Description: Prosody configuration for Jitsi Meet
|
||||
|
||||
Package: jitsi-meet-tokens
|
||||
Architecture: all
|
||||
Depends: ${misc:Depends}, prosody-trunk (>= 1nightly747) | prosody-0.11 | prosody (>= 0.11.2), libssl1.0-dev | libssl-dev, luarocks, jitsi-meet-prosody, git
|
||||
Depends: ${misc:Depends}, prosody-trunk (>= 1nightly747) | prosody-0.11 | prosody (>= 0.11.2), libssl-dev, luarocks, jitsi-meet-prosody
|
||||
Description: Prosody token authentication plugin for Jitsi Meet
|
||||
|
||||
Package: jitsi-meet-turnserver
|
||||
|
||||
11
debian/jitsi-meet-tokens.postinst
vendored
11
debian/jitsi-meet-tokens.postinst
vendored
@@ -48,9 +48,9 @@ case "$1" in
|
||||
db_stop
|
||||
|
||||
if [ -f "$PROSODY_HOST_CONFIG" ] ; then
|
||||
# search for the token auth, if this is not enabled this is the
|
||||
# search for --plugin_paths, if this is not enabled this is the
|
||||
# first time we install tokens package and needs a config change
|
||||
if ! egrep -q '^\s*authentication\s*=\s*"token"' "$PROSODY_HOST_CONFIG"; then
|
||||
if grep -q "\-\-plugin_paths" "$PROSODY_HOST_CONFIG"; then
|
||||
# enable tokens in prosody host config
|
||||
sed -i 's/--plugin_paths/plugin_paths/g' $PROSODY_HOST_CONFIG
|
||||
sed -i 's/authentication = "anonymous"/authentication = "token"/g' $PROSODY_HOST_CONFIG
|
||||
@@ -58,7 +58,6 @@ case "$1" in
|
||||
sed -i "s/ --app_id=\"example_app_id\"/ app_id=\"$APP_ID\"/g" $PROSODY_HOST_CONFIG
|
||||
sed -i "s/ --app_secret=\"example_app_secret\"/ app_secret=\"$APP_SECRET\"/g" $PROSODY_HOST_CONFIG
|
||||
sed -i 's/ --modules_enabled = { "token_verification" }/ modules_enabled = { "token_verification" }/g' $PROSODY_HOST_CONFIG
|
||||
sed -i '/^\s*--\s*"token_verification"/ s/--\s*//' $PROSODY_HOST_CONFIG
|
||||
|
||||
# Install luajwt
|
||||
if ! luarocks install luajwtjitsi; then
|
||||
@@ -74,9 +73,9 @@ case "$1" in
|
||||
PRTRUNK_INSTALL_CHECK="$(dpkg-query -f '${Status}' -W 'prosody-trunk' 2>/dev/null | awk '{print $3}' || true)"
|
||||
PR_VER_INSTALLED=$(dpkg-query -f='${Version}\n' --show prosody 2>/dev/null || true)
|
||||
if [ "$PR10_INSTALL_CHECK" = "installed" ] \
|
||||
|| [ "$PR10_INSTALL_CHECK" = "unpacked" ] \
|
||||
|| [ "$PRTRUNK_INSTALL_CHECK" = "installed" ] \
|
||||
|| [ "$PRTRUNK_INSTALL_CHECK" = "unpacked" ] \
|
||||
|| "$PR10_INSTALL_CHECK" = "unpacked" \
|
||||
|| "$PRTRUNK_INSTALL_CHECK" = "installed" \
|
||||
|| "$PRTRUNK_INSTALL_CHECK" = "unpacked" \
|
||||
|| dpkg --compare-versions "$PR_VER_INSTALLED" lt "0.11" ; then
|
||||
sed -i 's/module:hook_global(/module:hook(/g' /usr/share/jitsi-meet/prosody-plugins/mod_auth_token.lua
|
||||
fi
|
||||
|
||||
3
debian/jitsi-meet-tokens.postrm
vendored
3
debian/jitsi-meet-tokens.postrm
vendored
@@ -37,10 +37,11 @@ case "$1" in
|
||||
APP_SECRET=$RET
|
||||
|
||||
# Revert prosody config
|
||||
sed -i 's/plugin_paths/--plugin_paths/g' $PROSODY_HOST_CONFIG
|
||||
sed -i 's/authentication = "token"/authentication = "anonymous"/g' $PROSODY_HOST_CONFIG
|
||||
sed -i "s/ app_id=\"$APP_ID\"/ --app_id=\"example_app_id\"/g" $PROSODY_HOST_CONFIG
|
||||
sed -i "s/ app_secret=\"$APP_SECRET\"/ --app_secret=\"example_app_secret\"/g" $PROSODY_HOST_CONFIG
|
||||
sed -i '/^\s*"token_verification"/ s/"token_verification"/-- "token_verification"/' $PROSODY_HOST_CONFIG
|
||||
sed -i 's/ -- "token_verification"/ "token_verification"/g' $PROSODY_HOST_CONFIG
|
||||
|
||||
if [ -x "/etc/init.d/prosody" ]; then
|
||||
invoke-rc.d prosody restart || true
|
||||
|
||||
@@ -45,10 +45,8 @@ server {
|
||||
error_page 404 /static/404.html;
|
||||
|
||||
gzip on;
|
||||
gzip_types text/plain text/css application/javascript application/json image/x-icon application/octet-stream application/wasm;
|
||||
gzip_types text/plain text/css application/javascript application/json;
|
||||
gzip_vary on;
|
||||
gzip_proxied no-cache no-store private expired auth;
|
||||
gzip_min_length 512;
|
||||
|
||||
location = /config.js {
|
||||
alias /etc/jitsi/meet/jitsi-meet.example.com-config.js;
|
||||
@@ -63,11 +61,6 @@ server {
|
||||
{
|
||||
add_header 'Access-Control-Allow-Origin' '*';
|
||||
alias /usr/share/jitsi-meet/$1/$2;
|
||||
|
||||
# cache all versioned files
|
||||
if ($arg_v) {
|
||||
expires 1y;
|
||||
}
|
||||
}
|
||||
|
||||
# BOSH
|
||||
|
||||
@@ -14,12 +14,6 @@ server {
|
||||
ssi on;
|
||||
}
|
||||
|
||||
gzip on;
|
||||
gzip_types text/plain text/css application/javascript application/json image/x-icon application/octet-stream application/wasm;
|
||||
gzip_vary on;
|
||||
gzip_proxied no-cache no-store private expired auth;
|
||||
gzip_min_length 512;
|
||||
|
||||
# BOSH
|
||||
location /http-bind {
|
||||
proxy_pass http://localhost:5280/http-bind;
|
||||
|
||||
@@ -28,12 +28,6 @@ server {
|
||||
tcp_nodelay on;
|
||||
}
|
||||
|
||||
gzip on;
|
||||
gzip_types text/plain text/css application/javascript application/json image/x-icon application/octet-stream application/wasm;
|
||||
gzip_vary on;
|
||||
gzip_proxied no-cache no-store private expired auth;
|
||||
gzip_min_length 512;
|
||||
|
||||
location ~ ^/([^/?&:'"]+)$ {
|
||||
try_files $uri @root_path;
|
||||
}
|
||||
|
||||
9
jest.config.js
Normal file
9
jest.config.js
Normal file
@@ -0,0 +1,9 @@
|
||||
module.exports = {
|
||||
moduleFileExtensions: [
|
||||
'js'
|
||||
],
|
||||
testMatch: [
|
||||
'<rootDir>/react/**/?(*.)+(test)?(.web).js?(x)'
|
||||
],
|
||||
verbose: true
|
||||
};
|
||||
@@ -102,7 +102,6 @@
|
||||
"bandwidth": "Estimated bandwidth:",
|
||||
"bitrate": "Bitrate:",
|
||||
"bridgeCount": "Server count: ",
|
||||
"codecs": "Codecs (A/V): ",
|
||||
"connectedTo": "Connected to:",
|
||||
"e2e_rtt": "E2E RTT:",
|
||||
"framerate": "Frame rate:",
|
||||
@@ -217,7 +216,9 @@
|
||||
"kickParticipantDialog": "Are you sure you want to kick this participant?",
|
||||
"kickParticipantTitle": "Kick this participant?",
|
||||
"kickTitle": "Ouch! {{participantDisplayName}} kicked you out of the meeting",
|
||||
"leaveJibriQueue": "Exit queue",
|
||||
"liveStreaming": "Live Streaming",
|
||||
"leaveJibriQueueWarning": "Are you sure you would like to exit the queue?",
|
||||
"liveStreamingDisabledBecauseOfActiveRecordingTooltip": "Not possible while recording is active",
|
||||
"liveStreamingDisabledForGuestTooltip": "Guests can't start live streaming.",
|
||||
"liveStreamingDisabledTooltip": "Start live stream disabled.",
|
||||
@@ -504,7 +505,6 @@
|
||||
"poweredby": "powered by",
|
||||
"prejoin": {
|
||||
"audioAndVideoError": "Audio and video error:",
|
||||
"audioDeviceProblem": "There is a problem with your audio device",
|
||||
"audioOnlyError": "Audio error:",
|
||||
"audioTrackError": "Could not create audio track.",
|
||||
"calling": "Calling",
|
||||
@@ -512,25 +512,6 @@
|
||||
"callMeAtNumber": "Call me at this number:",
|
||||
"configuringDevices": "Configuring devices...",
|
||||
"connectedWithAudioQ": "You’re connected with audio?",
|
||||
"connection": {
|
||||
"good": "Your internet connection looks good!",
|
||||
"nonOptimal": "Your internet connection is not optimal",
|
||||
"poor": "You have a poor internet connection"
|
||||
},
|
||||
"connectionDetails": {
|
||||
"audioClipping": "We expect your audio to be clipped.",
|
||||
"audioHighQuality": "We expect your audio to have excellent quality.",
|
||||
"audioLowNoVideo": "We expect your audio quality to be low and no video.",
|
||||
"goodQuality": "Awesome! Your media quality is going to be great.",
|
||||
"noMediaConnectivity": "We could not find a way to establish media connectivity for this test. This is typically caused by a firewall or NAT.",
|
||||
"noVideo": "We expect that your video will be terrible.",
|
||||
"undetectable": "If you still can not make calls in browser, we recommend that you make sure your speakers, microphone and camera are properly set up, that you have granted your browser rights to use your microphone and camera, and that your browser version is up-to-date. If you still have trouble calling, you should contact the web application developer.",
|
||||
"veryPoorConnection": "We expect your call quality to be really terrible.",
|
||||
"videoFreezing": "We expect your video to freeze, turn black, and be pixelated.",
|
||||
"videoHighQuality": "We expect your video to have good quality.",
|
||||
"videoLowQuality": "We expect your video to have low quality in terms of frame rate and resolution.",
|
||||
"videoTearing": "We expect your video to be pixelated or have visual artefacts."
|
||||
},
|
||||
"copyAndShare": "Copy & share meeting link",
|
||||
"dialInMeeting": "Dial into the meeting",
|
||||
"dialInPin": "Dial into the meeting and enter PIN code:",
|
||||
@@ -540,7 +521,6 @@
|
||||
"errorDialOutDisconnected": "Could not dial out. Disconnected",
|
||||
"errorDialOutFailed": "Could not dial out. Call failed",
|
||||
"errorDialOutStatus": "Error getting dial out status",
|
||||
"errorMissingName": "Please enter your name to join the meeting",
|
||||
"errorStatusCode": "Error dialing out, status code: {{status}}",
|
||||
"errorValidation": "Number validation failed",
|
||||
"iWantToDialIn": "I want to dial in",
|
||||
@@ -930,5 +910,22 @@
|
||||
"passwordJoinButton": "Join",
|
||||
"reject": "Reject",
|
||||
"toggleLabel": "Enable lobby"
|
||||
},
|
||||
"jibriQueue": {
|
||||
"recording": {
|
||||
"title": "You have joined a recording queue!",
|
||||
"time": "Estimated time for starting the recording: {{time}}",
|
||||
"footer": "For unlimited recordings you should subscribe to 8x8 Meetings",
|
||||
"left": "You have left the recording queue!"
|
||||
},
|
||||
"livestreaming": {
|
||||
"title": "You have joined a live streaming queue!",
|
||||
"time": "Estimated time for starting the live streaming: {{time}}",
|
||||
"footer": "For unlimited live streaming you should subscribe to 8x8 Meetings",
|
||||
"left": "You have left the live streaming queue!"
|
||||
},
|
||||
"position": "{{count}} more person is waiting in front of you.",
|
||||
"position_plural": "{{count}} more people are waiting in front of you.",
|
||||
"exit": "Exit queue"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,6 +238,7 @@ function initCommands() {
|
||||
return;
|
||||
}
|
||||
|
||||
const jibriQueueJID = state['features/base/config'].jibriQueueJID;
|
||||
let recordingConfig;
|
||||
|
||||
if (mode === JitsiRecordingConstants.mode.FILE) {
|
||||
@@ -251,7 +252,8 @@ function initCommands() {
|
||||
'token': dropboxToken
|
||||
}
|
||||
}
|
||||
})
|
||||
}),
|
||||
jibriQueueJID
|
||||
};
|
||||
} else {
|
||||
recordingConfig = {
|
||||
@@ -260,12 +262,14 @@ function initCommands() {
|
||||
'file_recording_metadata': {
|
||||
'share': shouldShare
|
||||
}
|
||||
})
|
||||
}),
|
||||
jibriQueueJID
|
||||
};
|
||||
}
|
||||
} else if (mode === JitsiRecordingConstants.mode.STREAM) {
|
||||
recordingConfig = {
|
||||
broadcastId: youtubeBroadcastID,
|
||||
jibriQueueJID,
|
||||
mode: JitsiRecordingConstants.mode.STREAM,
|
||||
streamId: youtubeStreamKey
|
||||
};
|
||||
@@ -275,7 +279,9 @@ function initCommands() {
|
||||
return;
|
||||
}
|
||||
|
||||
conference.startRecording(recordingConfig);
|
||||
conference.startRecording(recordingConfig).catch(() => {
|
||||
// prevent unhandled promise rejection.
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -302,8 +308,10 @@ function initCommands() {
|
||||
|
||||
const activeSession = getActiveSession(state, mode);
|
||||
|
||||
if (activeSession && activeSession.id) {
|
||||
conference.stopRecording(activeSession.id);
|
||||
if (activeSession && (activeSession.id || activeSession.queueID)) {
|
||||
conference.stopRecording(activeSession.id, activeSession.queueID).catch(() => {
|
||||
// prevent unhandled promise rejection.
|
||||
});
|
||||
} else {
|
||||
logger.error('No recording or streaming session found');
|
||||
}
|
||||
|
||||
7302
package-lock.json
generated
7302
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -31,6 +31,7 @@
|
||||
"@atlaskit/tabs": "8.0.11",
|
||||
"@atlaskit/theme": "7.0.2",
|
||||
"@atlaskit/toggle": "5.0.14",
|
||||
"@atlaskit/portal": "4.0.0",
|
||||
"@atlaskit/tooltip": "12.1.13",
|
||||
"@jitsi/js-utils": "1.0.1",
|
||||
"@microsoft/microsoft-graph-client": "1.1.0",
|
||||
@@ -56,7 +57,7 @@
|
||||
"jquery-i18next": "1.2.1",
|
||||
"js-md5": "0.6.1",
|
||||
"jwt-decode": "2.2.0",
|
||||
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#65df5b1da6bb6934e9d42f50d95aae0df6a5fd90",
|
||||
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#f74cd0abe9c696a9c3ca7dbb9ca170e6e84d6756",
|
||||
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
|
||||
"lodash": "4.17.19",
|
||||
"moment": "2.19.4",
|
||||
@@ -90,7 +91,7 @@
|
||||
"redux": "4.0.4",
|
||||
"redux-thunk": "2.2.0",
|
||||
"rnnoise-wasm": "github:jitsi/rnnoise-wasm.git#566a16885897704d6e6d67a1d5ac5d39781db2af",
|
||||
"rtcstats": "github:jitsi/rtcstats#v6.2.0",
|
||||
"rtcstats": "github:jitsi/rtcstats#v6.1.3",
|
||||
"styled-components": "3.4.9",
|
||||
"util": "0.12.1",
|
||||
"uuid": "3.1.0",
|
||||
@@ -125,6 +126,7 @@
|
||||
"expose-loader": "0.7.5",
|
||||
"flow-bin": "0.104.0",
|
||||
"imports-loader": "0.7.1",
|
||||
"jest": "26.1.0",
|
||||
"jetifier": "1.6.4",
|
||||
"metro-react-native-babel-preset": "0.56.0",
|
||||
"node-sass": "4.14.1",
|
||||
@@ -144,6 +146,7 @@
|
||||
"scripts": {
|
||||
"lint": "eslint . && flow",
|
||||
"postinstall": "jetify",
|
||||
"test": "jest",
|
||||
"validate": "npm ls"
|
||||
},
|
||||
"browser": {
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import type { Dispatch } from 'redux';
|
||||
|
||||
import { API_ID } from '../../../modules/API/constants';
|
||||
import { setRoom } from '../base/conference';
|
||||
import {
|
||||
configWillLoad,
|
||||
@@ -24,7 +23,6 @@ import {
|
||||
parseURIString,
|
||||
toURLString
|
||||
} from '../base/util';
|
||||
import { isVpaasMeeting } from '../billing-counter/functions';
|
||||
import { clearNotifications, showNotification } from '../notifications';
|
||||
import { setFatalError } from '../overlay';
|
||||
|
||||
@@ -170,11 +168,9 @@ export function redirectWithStoredParams(pathname: string) {
|
||||
* window.location.pathname. If the specified pathname is relative, the context
|
||||
* root of the Web app will be prepended to the specified pathname before
|
||||
* assigning it to window.location.pathname.
|
||||
* @param {string} hashParam - Optional hash param to assign to
|
||||
* window.location.hash.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function redirectToStaticPage(pathname: string, hashParam: ?string) {
|
||||
export function redirectToStaticPage(pathname: string) {
|
||||
return () => {
|
||||
const windowLocation = window.location;
|
||||
let newPathname = pathname;
|
||||
@@ -188,10 +184,6 @@ export function redirectToStaticPage(pathname: string, hashParam: ?string) {
|
||||
newPathname = getLocationContextRoot(windowLocation) + newPathname;
|
||||
}
|
||||
|
||||
if (hashParam) {
|
||||
windowLocation.hash = hashParam;
|
||||
}
|
||||
|
||||
windowLocation.pathname = newPathname;
|
||||
};
|
||||
}
|
||||
@@ -292,14 +284,8 @@ export function maybeRedirectToWelcomePage(options: Object = {}) {
|
||||
|
||||
// if close page is enabled redirect to it, without further action
|
||||
if (enableClosePage) {
|
||||
if (isVpaasMeeting(getState())) {
|
||||
redirectToStaticPage('/');
|
||||
}
|
||||
|
||||
const { isGuest, jwt } = getState()['features/base/jwt'];
|
||||
|
||||
let hashParam;
|
||||
|
||||
// save whether current user is guest or not, and pass auth token,
|
||||
// before navigating to close page
|
||||
window.sessionStorage.setItem('guest', isGuest);
|
||||
@@ -308,15 +294,12 @@ export function maybeRedirectToWelcomePage(options: Object = {}) {
|
||||
let path = 'close.html';
|
||||
|
||||
if (interfaceConfig.SHOW_PROMOTIONAL_CLOSE_PAGE) {
|
||||
if (Number(API_ID) === API_ID) {
|
||||
hashParam = `#jitsi_meet_external_api_id=${API_ID}`;
|
||||
}
|
||||
path = 'close3.html';
|
||||
} else if (!options.feedbackSubmitted) {
|
||||
path = 'close2.html';
|
||||
}
|
||||
|
||||
dispatch(redirectToStaticPage(`static/${path}`, hashParam));
|
||||
dispatch(redirectToStaticPage(`static/${path}`));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ import '../base/sounds/middleware';
|
||||
import '../base/testing/middleware';
|
||||
import '../base/tracks/middleware';
|
||||
import '../base/user-interaction/middleware';
|
||||
import '../billing-counter/middleware';
|
||||
import '../calendar-sync/middleware';
|
||||
import '../chat/middleware';
|
||||
import '../conference/middleware';
|
||||
|
||||
@@ -17,7 +17,8 @@ import {
|
||||
AVATAR_ID_COMMAND,
|
||||
AVATAR_URL_COMMAND,
|
||||
EMAIL_COMMAND,
|
||||
JITSI_CONFERENCE_URL_KEY
|
||||
JITSI_CONFERENCE_URL_KEY,
|
||||
VIDEO_QUALITY_LEVELS
|
||||
} from './constants';
|
||||
import logger from './logger';
|
||||
|
||||
@@ -213,6 +214,38 @@ export function getCurrentConference(stateful: Function | Object) {
|
||||
return joining || passwordRequired || membersOnly;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the nearest match for the passed in {@link availableHeight} to am
|
||||
* enumerated value in {@code VIDEO_QUALITY_LEVELS}.
|
||||
*
|
||||
* @param {number} availableHeight - The height to which a matching video
|
||||
* quality level should be found.
|
||||
* @returns {number} The closest matching value from
|
||||
* {@code VIDEO_QUALITY_LEVELS}.
|
||||
*/
|
||||
export function getNearestReceiverVideoQualityLevel(availableHeight: number) {
|
||||
const qualityLevels = [
|
||||
VIDEO_QUALITY_LEVELS.HIGH,
|
||||
VIDEO_QUALITY_LEVELS.STANDARD,
|
||||
VIDEO_QUALITY_LEVELS.LOW
|
||||
];
|
||||
|
||||
let selectedLevel = qualityLevels[0];
|
||||
|
||||
for (let i = 1; i < qualityLevels.length; i++) {
|
||||
const previousValue = qualityLevels[i - 1];
|
||||
const currentValue = qualityLevels[i];
|
||||
const diffWithCurrent = Math.abs(availableHeight - currentValue);
|
||||
const diffWithPrevious = Math.abs(availableHeight - previousValue);
|
||||
|
||||
if (diffWithCurrent < diffWithPrevious) {
|
||||
selectedLevel = currentValue;
|
||||
}
|
||||
}
|
||||
|
||||
return selectedLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the stored room name.
|
||||
*
|
||||
|
||||
@@ -624,7 +624,7 @@ function _updateLocalParticipantInConference({ dispatch, getState }, next, actio
|
||||
|
||||
// When the local user role is updated to moderator and we have a pending subject change
|
||||
// which was not reflected we need to set it (the first time we tried was before becoming moderator).
|
||||
if (typeof pendingSubjectChange !== 'undefined' && pendingSubjectChange !== subject) {
|
||||
if (pendingSubjectChange !== subject) {
|
||||
dispatch(setSubject(pendingSubjectChange));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,8 +43,8 @@ export const SET_CONFIG = 'SET_CONFIG';
|
||||
* and the passed object.
|
||||
*
|
||||
* {
|
||||
* type: UPDATE_CONFIG,
|
||||
* type: _UPDATE_CONFIG,
|
||||
* config: Object
|
||||
* }
|
||||
*/
|
||||
export const UPDATE_CONFIG = 'UPDATE_CONFIG';
|
||||
export const _UPDATE_CONFIG = '_UPDATE_CONFIG';
|
||||
|
||||
@@ -6,24 +6,10 @@ import type { Dispatch } from 'redux';
|
||||
import { addKnownDomains } from '../known-domains';
|
||||
import { parseURIString } from '../util';
|
||||
|
||||
import { CONFIG_WILL_LOAD, LOAD_CONFIG_ERROR, SET_CONFIG, UPDATE_CONFIG } from './actionTypes';
|
||||
import { CONFIG_WILL_LOAD, LOAD_CONFIG_ERROR, SET_CONFIG } from './actionTypes';
|
||||
import { _CONFIG_STORE_PREFIX } from './constants';
|
||||
import { setConfigFromURLParams } from './functions';
|
||||
|
||||
|
||||
/**
|
||||
* Updates the config with new options.
|
||||
*
|
||||
* @param {Object} config - The new options (to add).
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function updateConfig(config: Object) {
|
||||
return {
|
||||
type: UPDATE_CONFIG,
|
||||
config
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals that the configuration (commonly known in Jitsi Meet as config.js)
|
||||
* for a specific locationURL will be loaded now.
|
||||
|
||||
@@ -69,7 +69,6 @@ export default [
|
||||
|
||||
'channelLastN',
|
||||
'constraints',
|
||||
'brandingRoomAlias',
|
||||
'debug',
|
||||
'debugAudioLevels',
|
||||
'defaultLanguage',
|
||||
@@ -101,14 +100,12 @@ export default [
|
||||
'enableInsecureRoomNameWarning',
|
||||
'enableLayerSuspension',
|
||||
'enableLipSync',
|
||||
'enableOpusRed',
|
||||
'enableRemb',
|
||||
'enableScreenshotCapture',
|
||||
'enableTalkWhileMuted',
|
||||
'enableNoAudioDetection',
|
||||
'enableNoisyMicDetection',
|
||||
'enableTcc',
|
||||
'enableAutomaticUrlCopy',
|
||||
'etherpad_base',
|
||||
'failICE',
|
||||
'feedbackPercentage',
|
||||
@@ -118,7 +115,6 @@ export default [
|
||||
'gatherStats',
|
||||
'googleApiApplicationClientID',
|
||||
'hiddenDomain',
|
||||
'hideLobbyButton',
|
||||
'hosts',
|
||||
'iAmRecorder',
|
||||
'iAmSipGateway',
|
||||
|
||||
@@ -8,8 +8,7 @@ import { addKnownDomains } from '../known-domains';
|
||||
import { MiddlewareRegistry } from '../redux';
|
||||
import { parseURIString } from '../util';
|
||||
|
||||
import { SET_CONFIG } from './actionTypes';
|
||||
import { updateConfig } from './actions';
|
||||
import { _UPDATE_CONFIG, SET_CONFIG } from './actionTypes';
|
||||
import { _CONFIG_STORE_PREFIX } from './constants';
|
||||
|
||||
/**
|
||||
@@ -115,7 +114,10 @@ function _setConfig({ dispatch, getState }, next, action) {
|
||||
config.resolution = resolutionFlag;
|
||||
}
|
||||
|
||||
dispatch(updateConfig(config));
|
||||
dispatch({
|
||||
type: _UPDATE_CONFIG,
|
||||
config
|
||||
});
|
||||
|
||||
// FIXME On Web we rely on the global 'config' variable which gets altered
|
||||
// multiple times, before it makes it to the reducer. At some point it may
|
||||
|
||||
@@ -4,7 +4,7 @@ import _ from 'lodash';
|
||||
|
||||
import { equals, ReducerRegistry, set } from '../redux';
|
||||
|
||||
import { UPDATE_CONFIG, CONFIG_WILL_LOAD, LOAD_CONFIG_ERROR, SET_CONFIG } from './actionTypes';
|
||||
import { _UPDATE_CONFIG, CONFIG_WILL_LOAD, LOAD_CONFIG_ERROR, SET_CONFIG } from './actionTypes';
|
||||
import { _cleanupConfig } from './functions';
|
||||
|
||||
/**
|
||||
@@ -50,7 +50,7 @@ const INITIAL_RN_STATE = {
|
||||
|
||||
ReducerRegistry.register('features/base/config', (state = _getInitialState(), action) => {
|
||||
switch (action.type) {
|
||||
case UPDATE_CONFIG:
|
||||
case _UPDATE_CONFIG:
|
||||
return _updateConfig(state, action);
|
||||
|
||||
case CONFIG_WILL_LOAD:
|
||||
|
||||
@@ -80,8 +80,12 @@ export function connect(id: ?string, password: ?string) {
|
||||
const state = getState();
|
||||
const options = _constructOptions(state);
|
||||
const { locationURL } = state['features/base/connection'];
|
||||
const { jwt } = state['features/base/jwt'];
|
||||
const connection = new JitsiMeetJS.JitsiConnection(options.appId, jwt, options);
|
||||
const { issuer, jwt } = state['features/base/jwt'];
|
||||
const connection
|
||||
= new JitsiMeetJS.JitsiConnection(
|
||||
options.appId,
|
||||
jwt && issuer && issuer !== 'anonymous' ? jwt : undefined,
|
||||
options);
|
||||
|
||||
connection[JITSI_CONNECTION_URL_KEY] = locationURL;
|
||||
|
||||
|
||||
@@ -54,17 +54,7 @@ export function getInviteURL(stateOrGetState: Function | Object): string {
|
||||
throw new Error('Can not get invite URL - the app is not ready');
|
||||
}
|
||||
|
||||
const { inviteDomain } = state['features/dynamic-branding'];
|
||||
const urlWithoutParams = getURLWithoutParams(locationURL);
|
||||
|
||||
if (inviteDomain) {
|
||||
const meetingId
|
||||
= state['features/base/config'].brandingRoomAlias || urlWithoutParams.pathname;
|
||||
|
||||
return `${inviteDomain}/${meetingId}`;
|
||||
}
|
||||
|
||||
return urlWithoutParams.href;
|
||||
return getURLWithoutParams(locationURL).href;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,6 +5,7 @@ import { processExternalDeviceRequest } from '../../device-selection';
|
||||
import { showNotification, showWarningNotification } from '../../notifications';
|
||||
import { replaceAudioTrackById, replaceVideoTrackById, setDeviceStatusWarning } from '../../prejoin/actions';
|
||||
import { isPrejoinPageVisible } from '../../prejoin/functions';
|
||||
import { CONFERENCE_JOINED } from '../conference';
|
||||
import { JitsiTrackErrors } from '../lib-jitsi-meet';
|
||||
import { MiddlewareRegistry } from '../redux';
|
||||
import { updateSettings } from '../settings';
|
||||
@@ -23,7 +24,6 @@ import {
|
||||
setVideoInputDevice
|
||||
} from './actions';
|
||||
import {
|
||||
areDeviceLabelsInitialized,
|
||||
formatDeviceLabel,
|
||||
groupDevicesByKind,
|
||||
setAudioOutputDeviceId
|
||||
@@ -73,6 +73,8 @@ function logDeviceList(deviceList) {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
switch (action.type) {
|
||||
case CONFERENCE_JOINED:
|
||||
return _conferenceJoined(store, next, action);
|
||||
case NOTIFY_CAMERA_ERROR: {
|
||||
if (typeof APP !== 'object' || !action.error) {
|
||||
break;
|
||||
@@ -146,9 +148,6 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
break;
|
||||
case UPDATE_DEVICE_LIST:
|
||||
logDeviceList(groupDevicesByKind(action.devices));
|
||||
if (areDeviceLabelsInitialized(store.getState())) {
|
||||
return _processPendingRequests(store, next, action);
|
||||
}
|
||||
break;
|
||||
case CHECK_AND_NOTIFY_FOR_NEW_DEVICE:
|
||||
_checkAndNotifyForNewDevice(store, action.newDevices, action.oldDevices);
|
||||
@@ -171,15 +170,11 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
* @private
|
||||
* @returns {Object} The value returned by {@code next(action)}.
|
||||
*/
|
||||
function _processPendingRequests({ dispatch, getState }, next, action) {
|
||||
function _conferenceJoined({ dispatch, getState }, next, action) {
|
||||
const result = next(action);
|
||||
const state = getState();
|
||||
const { pendingRequests } = state['features/base/devices'];
|
||||
|
||||
if (!pendingRequests || pendingRequests.length === 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
pendingRequests.forEach(request => {
|
||||
processExternalDeviceRequest(
|
||||
dispatch,
|
||||
|
||||
@@ -98,7 +98,4 @@ export { default as IconVolume } from './volume.svg';
|
||||
export { default as IconVolumeEmpty } from './volume-empty.svg';
|
||||
export { default as IconVolumeOff } from './volume-off.svg';
|
||||
export { default as IconWarning } from './warning.svg';
|
||||
export { default as IconWifi1Bar } from './wifi-1.svg';
|
||||
export { default as IconWifi2Bars } from './wifi-2.svg';
|
||||
export { default as IconWifi3Bars } from './wifi-3.svg';
|
||||
export { default as IconYahoo } from './yahoo.svg';
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
<svg width="16" height="12" viewBox="0 0 16 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path opacity="0.4" d="M13.0913 6.59847C12.4227 5.88894 11.629 5.32611 10.7554 4.94212C9.88182 4.55812 8.94553 4.36048 7.99997 4.36048C7.05442 4.36048 6.11813 4.55812 5.24456 4.94212C4.371 5.32611 3.57726 5.88894 2.90869 6.59847L4.36305 8.14176C4.84061 7.63486 5.4076 7.23276 6.03163 6.95842C6.65566 6.68408 7.32451 6.54288 7.99997 6.54288C8.67544 6.54288 9.34429 6.68408 9.96832 6.95842C10.5923 7.23276 11.1593 7.63486 11.6369 8.14176L13.0913 6.59847Z" fill="white"/>
|
||||
<path opacity="0.4" d="M16 3.51081C13.8766 1.26261 10.9996 0 8 0C5.00044 0 2.12337 1.26261 0 3.51081L1.45436 5.0541C3.19156 3.21432 5.54565 2.18105 8 2.18105C10.4543 2.18105 12.8084 3.21432 14.5456 5.0541L16 3.51081Z" fill="white"/>
|
||||
<path d="M5.94287 9.81713L7.99996 12L10.057 9.81713C9.78693 9.53041 9.46623 9.30297 9.11328 9.1478C8.76032 8.99263 8.38201 8.91276 7.99996 8.91276C7.6179 8.91276 7.23959 8.99263 6.88663 9.1478C6.53368 9.30297 6.21298 9.53041 5.94287 9.81713Z" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.0 KiB |
@@ -1,5 +0,0 @@
|
||||
<svg width="16" height="12" viewBox="0 0 16 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13.0913 6.59847C12.4227 5.88894 11.629 5.32611 10.7554 4.94211C9.88182 4.55811 8.94553 4.36047 7.99997 4.36047C7.05442 4.36047 6.11813 4.55811 5.24456 4.94211C4.371 5.32611 3.57726 5.88894 2.90869 6.59847L4.36305 8.14176C4.84061 7.63486 5.4076 7.23276 6.03163 6.95842C6.65566 6.68408 7.32451 6.54288 7.99997 6.54288C8.67544 6.54288 9.34429 6.68408 9.96832 6.95842C10.5923 7.23276 11.1593 7.63486 11.6369 8.14176L13.0913 6.59847Z" fill="white"/>
|
||||
<path opacity="0.4" d="M16 3.51081C13.8766 1.26261 10.9996 0 8 0C5.00044 0 2.12337 1.26261 0 3.51081L1.45436 5.0541C3.19156 3.21432 5.54565 2.18105 8 2.18105C10.4543 2.18105 12.8084 3.21432 14.5456 5.0541L16 3.51081Z" fill="white"/>
|
||||
<path d="M5.94287 9.81713L7.99996 12L10.057 9.81713C9.78693 9.53042 9.46623 9.30298 9.11328 9.14781C8.76032 8.99263 8.38201 8.91277 7.99996 8.91277C7.6179 8.91277 7.23959 8.99263 6.88663 9.14781C6.53368 9.30298 6.21298 9.53042 5.94287 9.81713Z" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.0 KiB |
@@ -1,5 +0,0 @@
|
||||
<svg width="16" height="12" viewBox="0 0 16 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13.0913 6.59847C12.4227 5.88894 11.629 5.32611 10.7554 4.94211C9.88182 4.55811 8.94553 4.36047 7.99997 4.36047C7.05442 4.36047 6.11813 4.55811 5.24456 4.94211C4.371 5.32611 3.57726 5.88894 2.90869 6.59847L4.36305 8.14176C4.84061 7.63486 5.4076 7.23276 6.03163 6.95842C6.65566 6.68408 7.32451 6.54288 7.99997 6.54288C8.67544 6.54288 9.34429 6.68408 9.96832 6.95842C10.5923 7.23276 11.1593 7.63486 11.6369 8.14176L13.0913 6.59847Z" fill="white"/>
|
||||
<path d="M16 3.51081C13.8766 1.26261 10.9996 0 8 0C5.00044 0 2.12337 1.26261 0 3.51081L1.45436 5.0541C3.19156 3.21432 5.54565 2.18105 8 2.18105C10.4543 2.18105 12.8084 3.21432 14.5456 5.0541L16 3.51081Z" fill="white"/>
|
||||
<path d="M5.94287 9.81713L7.99996 12L10.057 9.81713C9.78693 9.53042 9.46623 9.30298 9.11328 9.14781C8.76032 8.99263 8.38201 8.91277 7.99996 8.91277C7.6179 8.91277 7.23959 8.99263 6.88663 9.14781C6.53368 9.30298 6.21298 9.53042 5.94287 9.81713Z" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.0 KiB |
@@ -1,5 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import { getLogger } from '../logging/functions';
|
||||
|
||||
export default getLogger('features/base/jwt');
|
||||
@@ -13,7 +13,6 @@ import { MiddlewareRegistry } from '../redux';
|
||||
import { SET_JWT } from './actionTypes';
|
||||
import { setJWT } from './actions';
|
||||
import { parseJWTFromURLParams } from './functions';
|
||||
import logger from './logger';
|
||||
|
||||
declare var APP: Object;
|
||||
|
||||
@@ -134,13 +133,7 @@ function _setJWT(store, next, action) {
|
||||
|
||||
action.isGuest = !enableUserRolesBasedOnToken;
|
||||
|
||||
let jwtPayload;
|
||||
|
||||
try {
|
||||
jwtPayload = jwtDecode(jwt);
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
}
|
||||
const jwtPayload = jwtDecode(jwt);
|
||||
|
||||
if (jwtPayload) {
|
||||
const { context, iss } = jwtPayload;
|
||||
|
||||
103
react/features/base/lastn/functions.test.js
Normal file
103
react/features/base/lastn/functions.test.js
Normal file
@@ -0,0 +1,103 @@
|
||||
import { limitLastN, validateLastNLimits } from './functions';
|
||||
|
||||
describe('limitLastN', () => {
|
||||
it('handles undefined mapping', () => {
|
||||
expect(limitLastN(0, undefined)).toBe(undefined);
|
||||
});
|
||||
describe('when a correct limit mapping is given', () => {
|
||||
const limits = new Map();
|
||||
|
||||
limits.set(5, -1);
|
||||
limits.set(10, 8);
|
||||
limits.set(20, 5);
|
||||
|
||||
it('returns undefined when less participants that the first limit', () => {
|
||||
expect(limitLastN(2, limits)).toBe(undefined);
|
||||
});
|
||||
it('picks the first limit correctly', () => {
|
||||
expect(limitLastN(5, limits)).toBe(-1);
|
||||
expect(limitLastN(9, limits)).toBe(-1);
|
||||
});
|
||||
it('picks the middle limit correctly', () => {
|
||||
expect(limitLastN(10, limits)).toBe(8);
|
||||
expect(limitLastN(13, limits)).toBe(8);
|
||||
expect(limitLastN(19, limits)).toBe(8);
|
||||
});
|
||||
it('picks the top limit correctly', () => {
|
||||
expect(limitLastN(20, limits)).toBe(5);
|
||||
expect(limitLastN(23, limits)).toBe(5);
|
||||
expect(limitLastN(100, limits)).toBe(5);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateLastNLimits', () => {
|
||||
describe('validates the input by returning undefined', () => {
|
||||
it('if lastNLimits param is not an Object', () => {
|
||||
expect(validateLastNLimits(5)).toBe(undefined);
|
||||
});
|
||||
it('if any key is not a number', () => {
|
||||
const limits = {
|
||||
'abc': 8,
|
||||
5: -1,
|
||||
20: 5
|
||||
};
|
||||
|
||||
expect(validateLastNLimits(limits)).toBe(undefined);
|
||||
});
|
||||
it('if any value is not a number', () => {
|
||||
const limits = {
|
||||
8: 'something',
|
||||
5: -1,
|
||||
20: 5
|
||||
};
|
||||
|
||||
expect(validateLastNLimits(limits)).toBe(undefined);
|
||||
});
|
||||
it('if any value is null', () => {
|
||||
const limits = {
|
||||
1: 1,
|
||||
5: null,
|
||||
20: 5
|
||||
};
|
||||
|
||||
expect(validateLastNLimits(limits)).toBe(undefined);
|
||||
});
|
||||
it('if any value is undefined', () => {
|
||||
const limits = {
|
||||
1: 1,
|
||||
5: undefined,
|
||||
20: 5
|
||||
};
|
||||
|
||||
expect(validateLastNLimits(limits)).toBe(undefined);
|
||||
});
|
||||
it('if the map is empty', () => {
|
||||
expect(validateLastNLimits({})).toBe(undefined);
|
||||
});
|
||||
});
|
||||
it('sorts by the keys', () => {
|
||||
const mappingKeys = validateLastNLimits({
|
||||
10: 5,
|
||||
3: 3,
|
||||
5: 4
|
||||
}).keys();
|
||||
|
||||
expect(mappingKeys.next().value).toBe(3);
|
||||
expect(mappingKeys.next().value).toBe(5);
|
||||
expect(mappingKeys.next().value).toBe(10);
|
||||
expect(mappingKeys.next().done).toBe(true);
|
||||
});
|
||||
it('converts keys and values to numbers', () => {
|
||||
const mapping = validateLastNLimits({
|
||||
3: 3,
|
||||
5: 4,
|
||||
10: 5
|
||||
});
|
||||
|
||||
for (const key of mapping.keys()) {
|
||||
expect(typeof key).toBe('number');
|
||||
expect(typeof mapping.get(key)).toBe('number');
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -70,7 +70,6 @@ function ActionButton({
|
||||
{children}
|
||||
{hasOptions && <div
|
||||
className = 'options'
|
||||
data-testid = 'prejoin.joinOptions'
|
||||
onClick = { disabled ? undefined : onOptionsClick }>
|
||||
<Icon
|
||||
className = 'icon'
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { Avatar } from '../../../avatar';
|
||||
import { connect } from '../../../redux';
|
||||
import { calculateAvatarDimensions } from '../../functions';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The height of the window.
|
||||
*/
|
||||
height: number,
|
||||
|
||||
/**
|
||||
* The name of the participant (if any).
|
||||
*/
|
||||
name: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Component displaying the avatar for the premeeting screen.
|
||||
*
|
||||
* @param {Props} props - The props of the component.
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
function PremeetingAvatar({ height, name }: Props) {
|
||||
const { marginTop, size } = calculateAvatarDimensions(height);
|
||||
|
||||
if (size <= 5) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div style = {{ marginTop }}>
|
||||
<Avatar
|
||||
className = 'preview-avatar'
|
||||
displayName = { name }
|
||||
participantId = 'local'
|
||||
size = { size } />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the redux state to the React {@code Component} props.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {{
|
||||
* height: number
|
||||
* }}
|
||||
*/
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
height: state['features/base/responsive-ui'].clientHeight
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(PremeetingAvatar);
|
||||
@@ -1,104 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { translate } from '../../../i18n';
|
||||
import { Icon, IconArrowDownSmall, IconWifi1Bar, IconWifi2Bars, IconWifi3Bars } from '../../../icons';
|
||||
import { connect } from '../../../redux';
|
||||
import { CONNECTION_TYPE } from '../../constants';
|
||||
import { getConnectionData } from '../../functions';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* List of strings with details about the connection.
|
||||
*/
|
||||
connectionDetails: string[],
|
||||
|
||||
/**
|
||||
* The type of the connection. Can be: 'none', 'poor', 'nonOptimal' or 'good'.
|
||||
*/
|
||||
connectionType: string,
|
||||
|
||||
/**
|
||||
* Used for translation.
|
||||
*/
|
||||
t: Function
|
||||
}
|
||||
|
||||
const CONNECTION_TYPE_MAP = {
|
||||
[CONNECTION_TYPE.POOR]: {
|
||||
connectionClass: 'con-status--poor',
|
||||
icon: IconWifi1Bar,
|
||||
connectionText: 'prejoin.connection.poor'
|
||||
},
|
||||
[CONNECTION_TYPE.NON_OPTIMAL]: {
|
||||
connectionClass: 'con-status--non-optimal',
|
||||
icon: IconWifi2Bars,
|
||||
connectionText: 'prejoin.connection.nonOptimal'
|
||||
},
|
||||
[CONNECTION_TYPE.GOOD]: {
|
||||
connectionClass: 'con-status--good',
|
||||
icon: IconWifi3Bars,
|
||||
connectionText: 'prejoin.connection.good'
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Component displaying information related to the connection & audio/video quality.
|
||||
*
|
||||
* @param {Props} props - The props of the component.
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
function ConnectionStatus({ connectionDetails, t, connectionType }: Props) {
|
||||
if (connectionType === CONNECTION_TYPE.NONE) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { connectionClass, icon, connectionText } = CONNECTION_TYPE_MAP[connectionType];
|
||||
const [ showDetails, toggleDetails ] = useState(false);
|
||||
const arrowClassName = showDetails
|
||||
? 'con-status-arrow con-status-arrow--up'
|
||||
: 'con-status-arrow';
|
||||
const detailsText = connectionDetails.map(t).join(' ');
|
||||
|
||||
return (
|
||||
<div className = 'con-status'>
|
||||
<div className = 'con-status-container'>
|
||||
<div className = 'con-status-header'>
|
||||
<div className = { `con-status-circle ${connectionClass}` }>
|
||||
<Icon
|
||||
size = { 16 }
|
||||
src = { icon } />
|
||||
</div>
|
||||
<span className = 'con-status-text'>{t(connectionText)}</span>
|
||||
<Icon
|
||||
className = { arrowClassName }
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick = { () => toggleDetails(!showDetails) }
|
||||
size = { 24 }
|
||||
src = { IconArrowDownSmall } />
|
||||
</div>
|
||||
{ showDetails
|
||||
&& <div className = 'con-status-details'>{detailsText}</div> }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the redux state to the React {@code Component} props.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {Object}
|
||||
*/
|
||||
function mapStateToProps(state): Object {
|
||||
const { connectionDetails, connectionType } = getConnectionData(state);
|
||||
|
||||
return {
|
||||
connectionDetails,
|
||||
connectionType
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(mapStateToProps)(ConnectionStatus));
|
||||
@@ -18,13 +18,7 @@ type Props = {
|
||||
/**
|
||||
* Used for translation.
|
||||
*/
|
||||
t: Function,
|
||||
|
||||
/**
|
||||
* Used to determine if invitation link should be automatically copied
|
||||
* after creating a meeting.
|
||||
*/
|
||||
_enableAutomaticUrlCopy: boolean,
|
||||
t: Function
|
||||
};
|
||||
|
||||
type State = {
|
||||
@@ -64,7 +58,6 @@ class CopyMeetingUrl extends Component<Props, State> {
|
||||
this._hideLinkCopied = this._hideLinkCopied.bind(this);
|
||||
this._showCopyLink = this._showCopyLink.bind(this);
|
||||
this._showLinkCopied = this._showLinkCopied.bind(this);
|
||||
this._copyUrlAutomatically = this._copyUrlAutomatically.bind(this);
|
||||
}
|
||||
|
||||
_copyUrl: () => void;
|
||||
@@ -142,37 +135,6 @@ class CopyMeetingUrl extends Component<Props, State> {
|
||||
});
|
||||
}
|
||||
|
||||
_copyUrlAutomatically: () => void;
|
||||
|
||||
/**
|
||||
* Attempts to automatically copy invitation URL.
|
||||
* Document has to be focused in order for this to work.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_copyUrlAutomatically() {
|
||||
navigator.clipboard.writeText(this.props.url)
|
||||
.then(() => {
|
||||
this._showLinkCopied();
|
||||
window.setTimeout(this._hideLinkCopied, COPY_TIMEOUT);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#componentDidMount()}. Invoked
|
||||
* immediately before mounting occurs.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentDidMount() {
|
||||
const { _enableAutomaticUrlCopy } = this.props;
|
||||
|
||||
if (_enableAutomaticUrlCopy) {
|
||||
setTimeout(this._copyUrlAutomatically, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
@@ -215,11 +177,8 @@ class CopyMeetingUrl extends Component<Props, State> {
|
||||
* @returns {Object}
|
||||
*/
|
||||
function mapStateToProps(state) {
|
||||
const { enableAutomaticUrlCopy } = state['features/base/config'];
|
||||
|
||||
return {
|
||||
url: getCurrentConferenceUrl(state),
|
||||
_enableAutomaticUrlCopy: enableAutomaticUrlCopy || false
|
||||
url: getCurrentConferenceUrl(state)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -6,11 +6,6 @@ import { getFieldValue } from '../../../react';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* If the input should be focused on display.
|
||||
*/
|
||||
autoFocus?: boolean,
|
||||
|
||||
/**
|
||||
* Class name to be appended to the default class list.
|
||||
*/
|
||||
@@ -114,7 +109,6 @@ export default class InputField extends PureComponent<Props, State> {
|
||||
render() {
|
||||
return (
|
||||
<input
|
||||
autoFocus = { this.props.autoFocus }
|
||||
className = { `field ${this.state.focused ? 'focused' : ''} ${this.props.className || ''}` }
|
||||
data-testid = { this.props.testId ? this.props.testId : undefined }
|
||||
onBlur = { this._onBlur }
|
||||
|
||||
@@ -4,7 +4,6 @@ import React, { PureComponent } from 'react';
|
||||
|
||||
import { AudioSettingsButton, VideoSettingsButton } from '../../../../toolbox/components/web';
|
||||
|
||||
import ConnectionStatus from './ConnectionStatus';
|
||||
import CopyMeetingUrl from './CopyMeetingUrl';
|
||||
import Preview from './Preview';
|
||||
|
||||
@@ -83,7 +82,6 @@ export default class PreMeetingScreen extends PureComponent<Props> {
|
||||
<div
|
||||
className = 'premeeting-screen'
|
||||
id = 'lobby-screen'>
|
||||
<ConnectionStatus />
|
||||
<Preview
|
||||
name = { name }
|
||||
showAvatar = { showAvatar }
|
||||
|
||||
@@ -2,12 +2,11 @@
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { Avatar } from '../../../avatar';
|
||||
import { Video } from '../../../media';
|
||||
import { connect } from '../../../redux';
|
||||
import { getLocalVideoTrack } from '../../../tracks';
|
||||
|
||||
import PreviewAvatar from './Avatar';
|
||||
|
||||
export type Props = {
|
||||
|
||||
/**
|
||||
@@ -55,7 +54,13 @@ function Preview(props: Props) {
|
||||
<div
|
||||
className = 'no-video'
|
||||
id = 'preview'>
|
||||
<PreviewAvatar name = { name } />
|
||||
<div className = 'preview-avatar-container'>
|
||||
<Avatar
|
||||
className = 'preview-avatar'
|
||||
displayName = { name }
|
||||
participantId = 'local'
|
||||
size = { 200 } />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
// @flow
|
||||
|
||||
export const CONNECTION_TYPE = {
|
||||
GOOD: 'good',
|
||||
NON_OPTIMAL: 'nonOptimal',
|
||||
NONE: 'none',
|
||||
POOR: 'poor'
|
||||
};
|
||||
@@ -1,213 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import { findIndex } from 'lodash';
|
||||
|
||||
import { CONNECTION_TYPE } from './constants';
|
||||
|
||||
const LOSS_AUDIO_THRESHOLDS = [ 0.33, 0.05 ];
|
||||
const LOSS_VIDEO_THRESHOLDS = [ 0.33, 0.1, 0.05 ];
|
||||
|
||||
const THROUGHPUT_AUDIO_THRESHOLDS = [ 8, 20 ];
|
||||
const THROUGHPUT_VIDEO_THRESHOLDS = [ 60, 750 ];
|
||||
|
||||
/**
|
||||
* The avatar size to container size ration.
|
||||
*/
|
||||
const ratio = 1 / 3;
|
||||
|
||||
/**
|
||||
* The max avatar size.
|
||||
*/
|
||||
const maxSize = 190;
|
||||
|
||||
/**
|
||||
* The window limit hight over which the avatar should have the default dimension.
|
||||
*/
|
||||
const upperHeightLimit = 760;
|
||||
|
||||
/**
|
||||
* The window limit hight under which the avatar should not be resized anymore.
|
||||
*/
|
||||
const lowerHeightLimit = 460;
|
||||
|
||||
/**
|
||||
* The default top margin of the avatar.
|
||||
*/
|
||||
const defaultMarginTop = '10%';
|
||||
|
||||
/**
|
||||
* The top margin of the avatar when its dimension is small.
|
||||
*/
|
||||
const smallMarginTop = '5%';
|
||||
|
||||
/**
|
||||
* Calculates avatar dimensions based on window height and position.
|
||||
*
|
||||
* @param {number} height - The window height.
|
||||
* @returns {{
|
||||
* marginTop: string,
|
||||
* size: number
|
||||
* }}
|
||||
*/
|
||||
export function calculateAvatarDimensions(height: number) {
|
||||
if (height > upperHeightLimit) {
|
||||
return {
|
||||
size: maxSize,
|
||||
marginTop: defaultMarginTop
|
||||
};
|
||||
}
|
||||
|
||||
if (height > lowerHeightLimit) {
|
||||
const diff = height - lowerHeightLimit;
|
||||
const percent = diff * ratio;
|
||||
const size = Math.floor(maxSize * percent / 100);
|
||||
let marginTop = defaultMarginTop;
|
||||
|
||||
if (height < 600) {
|
||||
marginTop = smallMarginTop;
|
||||
}
|
||||
|
||||
return {
|
||||
size,
|
||||
marginTop
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
size: 0,
|
||||
marginTop: '0'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the level based on a list of thresholds.
|
||||
*
|
||||
* @param {number[]} thresholds - The thresholds array.
|
||||
* @param {number} value - The value against which the level is calculated.
|
||||
* @param {boolean} descending - The order based on which the level is calculated.
|
||||
*
|
||||
* @returns {number}
|
||||
*/
|
||||
function _getLevel(thresholds, value, descending = true) {
|
||||
let predicate;
|
||||
|
||||
if (descending) {
|
||||
predicate = function(threshold) {
|
||||
return value > threshold;
|
||||
};
|
||||
} else {
|
||||
predicate = function(threshold) {
|
||||
return value < threshold;
|
||||
};
|
||||
}
|
||||
|
||||
const i = findIndex(thresholds, predicate);
|
||||
|
||||
if (i === -1) {
|
||||
return thresholds.length;
|
||||
}
|
||||
|
||||
return i;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the connection details from the test results.
|
||||
*
|
||||
* @param {{
|
||||
* fractionalLoss: number,
|
||||
* throughput: number
|
||||
* }} testResults - The state of the app.
|
||||
*
|
||||
* @returns {{
|
||||
* connectionType: string,
|
||||
* connectionDetails: string[]
|
||||
* }}
|
||||
*/
|
||||
function _getConnectionDataFromTestResults({ fractionalLoss: l, throughput: t }) {
|
||||
const loss = {
|
||||
audioQuality: _getLevel(LOSS_AUDIO_THRESHOLDS, l),
|
||||
videoQuality: _getLevel(LOSS_VIDEO_THRESHOLDS, l)
|
||||
};
|
||||
const throughput = {
|
||||
audioQuality: _getLevel(THROUGHPUT_AUDIO_THRESHOLDS, t, false),
|
||||
videoQuality: _getLevel(THROUGHPUT_VIDEO_THRESHOLDS, t, false)
|
||||
};
|
||||
let connectionType = CONNECTION_TYPE.NONE;
|
||||
const connectionDetails = [];
|
||||
|
||||
if (throughput.audioQuality === 0 || loss.audioQuality === 0) {
|
||||
// Calls are impossible.
|
||||
connectionType = CONNECTION_TYPE.POOR;
|
||||
connectionDetails.push('prejoin.connectionDetails.veryPoorConnection');
|
||||
} else if (
|
||||
throughput.audioQuality === 2
|
||||
&& throughput.videoQuality === 2
|
||||
&& loss.audioQuality === 2
|
||||
&& loss.videoQuality === 3
|
||||
) {
|
||||
// Ideal conditions for both audio and video. Show only one message.
|
||||
connectionType = CONNECTION_TYPE.GOOD;
|
||||
connectionDetails.push('prejoin.connectionDetails.goodQuality');
|
||||
} else {
|
||||
connectionType = CONNECTION_TYPE.NON_OPTIMAL;
|
||||
|
||||
if (throughput.audioQuality === 1) {
|
||||
// Minimum requirements for a call are met.
|
||||
connectionDetails.push('prejoin.connectionDetails.audioLowNoVideo');
|
||||
} else {
|
||||
// There are two paragraphs: one saying something about audio and the other about video.
|
||||
if (loss.audioQuality === 1) {
|
||||
connectionDetails.push('prejoin.connectionDetails.audioClipping');
|
||||
} else {
|
||||
connectionDetails.push('prejoin.connectionDetails.audioHighQuality');
|
||||
}
|
||||
|
||||
if (throughput.videoQuality === 0 || loss.videoQuality === 0) {
|
||||
connectionDetails.push('prejoin.connectionDetails.noVideo');
|
||||
} else if (throughput.videoQuality === 1) {
|
||||
connectionDetails.push('prejoin.connectionDetails.videoLowQuality');
|
||||
} else if (loss.videoQuality === 1) {
|
||||
connectionDetails.push('prejoin.connectionDetails.videoFreezing');
|
||||
} else if (loss.videoQuality === 2) {
|
||||
connectionDetails.push('prejoin.connectionDetails.videoTearing');
|
||||
} else {
|
||||
connectionDetails.push('prejoin.connectionDetails.videoHighQuality');
|
||||
}
|
||||
}
|
||||
connectionDetails.push('prejoin.connectionDetails.undetectable');
|
||||
}
|
||||
|
||||
return {
|
||||
connectionType,
|
||||
connectionDetails
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector for determining the connection type & details.
|
||||
*
|
||||
* @param {Object} state - The state of the app.
|
||||
* @returns {{
|
||||
* connectionType: string,
|
||||
* connectionDetails: string[]
|
||||
* }}
|
||||
*/
|
||||
export function getConnectionData(state: Object) {
|
||||
const { precallTestResults } = state['features/prejoin'];
|
||||
|
||||
if (precallTestResults) {
|
||||
if (precallTestResults.mediaConnectivity) {
|
||||
return _getConnectionDataFromTestResults(precallTestResults);
|
||||
}
|
||||
|
||||
return {
|
||||
connectionType: CONNECTION_TYPE.POOR,
|
||||
connectionDetails: [ 'prejoin.connectionDetails.noMediaConnectivity' ]
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
connectionType: CONNECTION_TYPE.NONE,
|
||||
connectionDetails: []
|
||||
};
|
||||
}
|
||||
@@ -2,11 +2,9 @@
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { isVpaasMeeting } from '../../../../billing-counter/functions';
|
||||
import { translate } from '../../../i18n';
|
||||
import { connect } from '../../../redux';
|
||||
|
||||
|
||||
declare var interfaceConfig: Object;
|
||||
|
||||
/**
|
||||
@@ -38,11 +36,6 @@ type Props = {
|
||||
*/
|
||||
_isGuest: boolean,
|
||||
|
||||
/**
|
||||
* Whether or not the current meeting is a vpaas one.
|
||||
*/
|
||||
_isVpaas: boolean,
|
||||
|
||||
/**
|
||||
* Flag used to signal that the logo can be displayed.
|
||||
* It becomes true after the user customization options are fetched.
|
||||
@@ -188,33 +181,6 @@ class Watermarks extends Component<Props, State> {
|
||||
|| _welcomePageIsVisible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the background image style.
|
||||
*
|
||||
* @private
|
||||
* @returns {string}
|
||||
*/
|
||||
_getBackgroundImageStyle() {
|
||||
const {
|
||||
_customLogoUrl,
|
||||
_isVpaas,
|
||||
defaultJitsiLogoURL
|
||||
} = this.props;
|
||||
let style = 'none';
|
||||
|
||||
if (_isVpaas) {
|
||||
if (_customLogoUrl) {
|
||||
style = `url(${_customLogoUrl})`;
|
||||
}
|
||||
} else {
|
||||
style = `url(${_customLogoUrl
|
||||
|| defaultJitsiLogoURL
|
||||
|| interfaceConfig.DEFAULT_LOGO_URL})`;
|
||||
}
|
||||
|
||||
return style;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a brand watermark if it is enabled.
|
||||
*
|
||||
@@ -255,22 +221,18 @@ class Watermarks extends Component<Props, State> {
|
||||
*/
|
||||
_renderJitsiWatermark() {
|
||||
let reactElement = null;
|
||||
const {
|
||||
_customLogoUrl,
|
||||
_customLogoLink,
|
||||
defaultJitsiLogoURL
|
||||
} = this.props;
|
||||
|
||||
if (this._canDisplayJitsiWatermark()) {
|
||||
const backgroundImage = this._getBackgroundImageStyle();
|
||||
const link = this.props._customLogoLink || this.state.jitsiWatermarkLink;
|
||||
const additionalStyles = {};
|
||||
|
||||
if (backgroundImage === 'none') {
|
||||
additionalStyles.height = 0;
|
||||
additionalStyles.width = 0;
|
||||
}
|
||||
|
||||
const link = _customLogoLink || this.state.jitsiWatermarkLink;
|
||||
const style = {
|
||||
backgroundImage,
|
||||
backgroundImage: `url(${_customLogoUrl || defaultJitsiLogoURL || interfaceConfig.DEFAULT_LOGO_URL})`,
|
||||
maxWidth: 140,
|
||||
maxHeight: 70,
|
||||
...additionalStyles
|
||||
maxHeight: 70
|
||||
};
|
||||
|
||||
reactElement = (<div
|
||||
@@ -337,7 +299,6 @@ function _mapStateToProps(state) {
|
||||
_customLogoLink: logoClickUrl,
|
||||
_customLogoUrl: logoImageUrl,
|
||||
_isGuest: isGuest,
|
||||
_isVpaas: isVpaasMeeting(state),
|
||||
_readyToDisplayJitsiWatermark: customizationReady,
|
||||
_welcomePageIsVisible: !room
|
||||
};
|
||||
|
||||
@@ -9,7 +9,6 @@ import { MiddlewareRegistry } from '../redux';
|
||||
import { parseURLParams } from '../util';
|
||||
|
||||
import { SETTINGS_UPDATED } from './actionTypes';
|
||||
import { updateSettings } from './actions';
|
||||
import { handleCallIntegrationChange, handleCrashReportingChange } from './functions';
|
||||
|
||||
/**
|
||||
@@ -161,18 +160,10 @@ function _updateLocalParticipantFromUrl({ dispatch, getState }) {
|
||||
const localParticipant = getLocalParticipant(getState());
|
||||
|
||||
if (localParticipant) {
|
||||
const displayName = _.escape(urlDisplayName);
|
||||
const email = _.escape(urlEmail);
|
||||
|
||||
dispatch(participantUpdated({
|
||||
...localParticipant,
|
||||
email,
|
||||
name: displayName
|
||||
}));
|
||||
|
||||
dispatch(updateSettings({
|
||||
displayName,
|
||||
email
|
||||
email: _.escape(urlEmail),
|
||||
name: _.escape(urlDisplayName)
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
/**
|
||||
* Action used to store the billing id.
|
||||
*/
|
||||
export const SET_BILLING_ID = 'SET_BILLING_ID';
|
||||
@@ -1,51 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import uuid from 'uuid';
|
||||
|
||||
import { SET_BILLING_ID } from './actionTypes';
|
||||
import { extractVpaasTenantFromPath, getBillingId, sendCountRequest } from './functions';
|
||||
|
||||
/**
|
||||
* Sends a billing count request when needed.
|
||||
* If there is no billingId, it presists one first and sends the request after.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function countEndpoint() {
|
||||
return function(dispatch: Function, getState: Function) {
|
||||
const state = getState();
|
||||
const baseUrl = state['features/base/config'].billingCounterUrl;
|
||||
const jwt = state['features/base/jwt'].jwt;
|
||||
const tenant = extractVpaasTenantFromPath(state['features/base/connection'].locationURL.pathname);
|
||||
const shouldSendRequest = Boolean(baseUrl && jwt && tenant);
|
||||
|
||||
if (shouldSendRequest) {
|
||||
let billingId = getBillingId();
|
||||
|
||||
if (!billingId) {
|
||||
billingId = uuid.v4();
|
||||
dispatch(setBillingId(billingId));
|
||||
}
|
||||
|
||||
sendCountRequest({
|
||||
baseUrl,
|
||||
billingId,
|
||||
jwt,
|
||||
tenant
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action used to set the user billing id.
|
||||
*
|
||||
* @param {string} value - The uid.
|
||||
* @returns {Object}
|
||||
*/
|
||||
function setBillingId(value) {
|
||||
return {
|
||||
type: SET_BILLING_ID,
|
||||
value
|
||||
};
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
/**
|
||||
* The key for the billing id stored in localStorage.
|
||||
*/
|
||||
export const BILLING_ID = 'billingId';
|
||||
|
||||
/**
|
||||
* The prefix for the vpaas tenant.
|
||||
*/
|
||||
export const VPAAS_TENANT_PREFIX = 'vpaas-magic-cookie';
|
||||
@@ -1,91 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import { jitsiLocalStorage } from '@jitsi/js-utils';
|
||||
|
||||
import { BILLING_ID, VPAAS_TENANT_PREFIX } from './constants';
|
||||
import logger from './logger';
|
||||
|
||||
/**
|
||||
* Returns the full vpaas tenant if available, given a path.
|
||||
*
|
||||
* @param {string} path - The meeting url path.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function extractVpaasTenantFromPath(path: string) {
|
||||
const [ , tenant ] = path.split('/');
|
||||
|
||||
if (tenant.startsWith(VPAAS_TENANT_PREFIX)) {
|
||||
return tenant;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the current meeting is a vpaas one.
|
||||
*
|
||||
* @param {Object} state - The state of the app.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isVpaasMeeting(state: Object) {
|
||||
return Boolean(
|
||||
state['features/base/config'].billingCounterUrl
|
||||
&& state['features/base/jwt'].jwt
|
||||
&& extractVpaasTenantFromPath(
|
||||
state['features/base/connection'].locationURL.pathname)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a billing counter request.
|
||||
*
|
||||
* @param {Object} reqData - The request info.
|
||||
* @param {string} reqData.baseUrl - The base url for the request.
|
||||
* @param {string} billingId - The unique id of the client.
|
||||
* @param {string} jwt - The JWT token.
|
||||
* @param {string} tenat - The client tenant.
|
||||
* @returns {void}
|
||||
*/
|
||||
export async function sendCountRequest({ baseUrl, billingId, jwt, tenant }: {
|
||||
baseUrl: string,
|
||||
billingId: string,
|
||||
jwt: string,
|
||||
tenant: string
|
||||
}) {
|
||||
const fullUrl = `${baseUrl}/${encodeURIComponent(tenant)}/${billingId}`;
|
||||
const headers = {
|
||||
'Authorization': `Bearer ${jwt}`
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetch(fullUrl, {
|
||||
method: 'GET',
|
||||
headers
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
logger.error('Status error:', res.status);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('Could not send request', err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the stored billing id.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getBillingId() {
|
||||
return jitsiLocalStorage.getItem(BILLING_ID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the billing id.
|
||||
*
|
||||
* @param {string} value - The id to be stored.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function setBillingId(value: string) {
|
||||
jitsiLocalStorage.setItem(BILLING_ID, value);
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import { getLogger } from '../base/logging/functions';
|
||||
|
||||
export default getLogger('features/billing-counter');
|
||||
@@ -1,31 +0,0 @@
|
||||
import { CONFERENCE_JOINED } from '../base/conference/actionTypes';
|
||||
import { MiddlewareRegistry } from '../base/redux';
|
||||
|
||||
import { SET_BILLING_ID } from './actionTypes';
|
||||
import { countEndpoint } from './actions';
|
||||
import { setBillingId } from './functions';
|
||||
|
||||
/**
|
||||
* The redux middleware for billing counter.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @returns {Function}
|
||||
*/
|
||||
MiddlewareRegistry.register(store => next => async action => {
|
||||
switch (action.type) {
|
||||
case SET_BILLING_ID: {
|
||||
setBillingId(action.value);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case CONFERENCE_JOINED: {
|
||||
store.dispatch(countEndpoint());
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return next(action);
|
||||
});
|
||||
@@ -16,7 +16,6 @@ import { translate } from '../../base/i18n';
|
||||
import { Icon, IconClose } from '../../base/icons';
|
||||
import { browser } from '../../base/lib-jitsi-meet';
|
||||
import { connect } from '../../base/redux';
|
||||
import { isVpaasMeeting } from '../../billing-counter/functions';
|
||||
import logger from '../logger';
|
||||
|
||||
|
||||
@@ -51,11 +50,6 @@ type Props = {
|
||||
*/
|
||||
iAmRecorder: boolean,
|
||||
|
||||
/**
|
||||
* Whether it's a vpaas meeting or not.
|
||||
*/
|
||||
isVpaas: boolean,
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
@@ -152,8 +146,7 @@ class ChromeExtensionBanner extends PureComponent<Props, State> {
|
||||
_isSupportedEnvironment() {
|
||||
return interfaceConfig.SHOW_CHROME_EXTENSION_BANNER
|
||||
&& browser.isChrome()
|
||||
&& !isMobileBrowser()
|
||||
&& !this.props.isVpaas;
|
||||
&& !isMobileBrowser();
|
||||
}
|
||||
|
||||
_onClosePressed: () => void;
|
||||
@@ -287,8 +280,7 @@ const _mapStateToProps = state => {
|
||||
// Using emptyObject so that we don't change the reference every time when _mapStateToProps is called.
|
||||
bannerCfg: state['features/base/config'].chromeExtensionBanner || emptyObject,
|
||||
conference: getCurrentConference(state),
|
||||
iAmRecorder: state['features/base/config'].iAmRecorder,
|
||||
isVpaas: isVpaasMeeting(state)
|
||||
iAmRecorder: state['features/base/config'].iAmRecorder
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -340,7 +340,6 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, State> {
|
||||
bandwidth,
|
||||
bitrate,
|
||||
bridgeCount,
|
||||
codec,
|
||||
e2eRtt,
|
||||
framerate,
|
||||
maxEnabledResolution,
|
||||
@@ -356,7 +355,6 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, State> {
|
||||
bandwidth = { bandwidth }
|
||||
bitrate = { bitrate }
|
||||
bridgeCount = { bridgeCount }
|
||||
codec = { codec }
|
||||
connectionSummary = { this._getConnectionStatusTip() }
|
||||
e2eRtt = { e2eRtt }
|
||||
framerate = { framerate }
|
||||
|
||||
@@ -122,7 +122,6 @@ const statsEmitter = {
|
||||
_onStatsUpdated(localUserId: string, stats: Object) {
|
||||
const allUserFramerates = stats.framerate || {};
|
||||
const allUserResolutions = stats.resolution || {};
|
||||
const allUserCodecs = stats.codec || {};
|
||||
|
||||
// FIXME resolution and framerate are maps keyed off of user ids with
|
||||
// stat values. Receivers of stats expect resolution and framerate to
|
||||
@@ -130,8 +129,7 @@ const statsEmitter = {
|
||||
// stats objects.
|
||||
const modifiedLocalStats = Object.assign({}, stats, {
|
||||
framerate: allUserFramerates[localUserId],
|
||||
resolution: allUserResolutions[localUserId],
|
||||
codec: allUserCodecs[localUserId]
|
||||
resolution: allUserResolutions[localUserId]
|
||||
});
|
||||
|
||||
this._emitStatsUpdate(localUserId, modifiedLocalStats);
|
||||
@@ -140,9 +138,8 @@ const statsEmitter = {
|
||||
// and update remote user stats as needed.
|
||||
const framerateUserIds = Object.keys(allUserFramerates);
|
||||
const resolutionUserIds = Object.keys(allUserResolutions);
|
||||
const codecUserIds = Object.keys(allUserCodecs);
|
||||
|
||||
_.union(framerateUserIds, resolutionUserIds, codecUserIds)
|
||||
_.union(framerateUserIds, resolutionUserIds)
|
||||
.filter(id => id !== localUserId)
|
||||
.forEach(id => {
|
||||
const remoteUserStats = {};
|
||||
@@ -159,12 +156,6 @@ const statsEmitter = {
|
||||
remoteUserStats.resolution = resolution;
|
||||
}
|
||||
|
||||
const codec = allUserCodecs[id];
|
||||
|
||||
if (codec) {
|
||||
remoteUserStats.codec = codec;
|
||||
}
|
||||
|
||||
this._emitStatsUpdate(id, remoteUserStats);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -34,11 +34,6 @@ type Props = {
|
||||
*/
|
||||
bridgeCount: number,
|
||||
|
||||
/**
|
||||
* Audio/video codecs in use for the connection.
|
||||
*/
|
||||
codec: Object,
|
||||
|
||||
/**
|
||||
* A message describing the connection quality.
|
||||
*/
|
||||
@@ -224,45 +219,6 @@ class ConnectionStatsTable extends Component<Props> {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a a table row as a ReactElement for displaying codec, if present.
|
||||
* This will typically be something like "Codecs (A/V): Opus, vp8".
|
||||
*
|
||||
* @private
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderCodecs() {
|
||||
const { codec, t } = this.props;
|
||||
|
||||
if (!codec) {
|
||||
return;
|
||||
}
|
||||
|
||||
let codecString;
|
||||
|
||||
// Only report one codec, in case there are multiple for a user.
|
||||
Object.keys(codec || {})
|
||||
.forEach(ssrc => {
|
||||
const { audio, video } = codec[ssrc];
|
||||
|
||||
codecString = `${audio}, ${video}`;
|
||||
});
|
||||
|
||||
if (!codecString) {
|
||||
codecString = 'N/A';
|
||||
}
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td>
|
||||
<span>{ t('connectionindicator.codecs') }</span>
|
||||
</td>
|
||||
<td>{ codecString }</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a table row as a ReactElement for displaying a summary message
|
||||
* about the current connection status.
|
||||
@@ -496,7 +452,6 @@ class ConnectionStatsTable extends Component<Props> {
|
||||
{ isRemoteVideo ? this._renderRegion() : null }
|
||||
{ this._renderResolution() }
|
||||
{ this._renderFrameRate() }
|
||||
{ this._renderCodecs() }
|
||||
{ isRemoteVideo ? null : this._renderBridgeCount() }
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import { isMobileBrowser } from '../base/environment/utils';
|
||||
import { Platform } from '../base/react';
|
||||
import { URI_PROTOCOL_PATTERN } from '../base/util';
|
||||
import { isVpaasMeeting } from '../billing-counter/functions';
|
||||
|
||||
import {
|
||||
DeepLinkingDesktopPage,
|
||||
@@ -54,7 +53,7 @@ export function getDeepLinkingPage(state) {
|
||||
const { launchInWeb } = state['features/deep-linking'];
|
||||
|
||||
// Show only if we are about to join a conference.
|
||||
if (launchInWeb || !room || state['features/base/config'].disableDeepLinking || isVpaasMeeting(state)) {
|
||||
if (launchInWeb || !room || state['features/base/config'].disableDeepLinking) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
|
||||
@@ -86,6 +86,7 @@ export function processExternalDeviceRequest( // eslint-disable-line max-params
|
||||
}
|
||||
const state = getState();
|
||||
const settings = state['features/base/settings'];
|
||||
const { conference } = state['features/base/conference'];
|
||||
let result = true;
|
||||
|
||||
switch (request.name) {
|
||||
@@ -164,7 +165,7 @@ export function processExternalDeviceRequest( // eslint-disable-line max-params
|
||||
case 'setDevice': {
|
||||
const { device } = request;
|
||||
|
||||
if (!areDeviceLabelsInitialized(state)) {
|
||||
if (!conference) {
|
||||
dispatch(addPendingDeviceRequest({
|
||||
type: 'devices',
|
||||
name: 'setDevice',
|
||||
|
||||
@@ -53,6 +53,7 @@ function setDynamicBrandingData(value) {
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Action used to signal the branding elements are ready to be displayed.
|
||||
*
|
||||
|
||||
@@ -14,7 +14,6 @@ const DEFAULT_STATE = {
|
||||
backgroundColor: '',
|
||||
backgroundImageUrl: '',
|
||||
customizationReady: false,
|
||||
inviteDomain: '',
|
||||
logoClickUrl: '',
|
||||
logoImageUrl: ''
|
||||
};
|
||||
@@ -25,12 +24,11 @@ const DEFAULT_STATE = {
|
||||
ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STATE, action) => {
|
||||
switch (action.type) {
|
||||
case SET_DYNAMIC_BRANDING_DATA: {
|
||||
const { backgroundColor, backgroundImageUrl, inviteDomain, logoClickUrl, logoImageUrl } = action.value;
|
||||
const { backgroundColor, backgroundImageUrl, logoClickUrl, logoImageUrl } = action.value;
|
||||
|
||||
return {
|
||||
backgroundColor,
|
||||
backgroundImageUrl,
|
||||
inviteDomain,
|
||||
logoClickUrl,
|
||||
logoImageUrl,
|
||||
customizationReady: true
|
||||
|
||||
@@ -66,12 +66,11 @@ class LobbyModeButton extends AbstractButton<Props, any> {
|
||||
export function _mapStateToProps(state: Object): $Shape<Props> {
|
||||
const conference = getCurrentConference(state);
|
||||
const { lobbyEnabled } = state['features/lobby'];
|
||||
const { hideLobbyButton } = state['features/base/config'];
|
||||
const lobbySupported = conference && conference.isLobbySupported();
|
||||
|
||||
return {
|
||||
lobbyEnabled,
|
||||
visible: lobbySupported && isLocalParticipantModerator(state) && !hideLobbyButton
|
||||
visible: lobbySupported && isLocalParticipantModerator(state)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -132,12 +132,10 @@ class LobbySection extends PureComponent<Props, State> {
|
||||
*/
|
||||
function mapStateToProps(state: Object): $Shape<Props> {
|
||||
const { conference } = state['features/base/conference'];
|
||||
const { hideLobbyButton } = state['features/base/config'];
|
||||
|
||||
return {
|
||||
_lobbyEnabled: state['features/lobby'].lobbyEnabled,
|
||||
_visible: conference && conference.isLobbySupported() && isLocalParticipantModerator(state)
|
||||
&& !hideLobbyButton
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// @flow
|
||||
|
||||
import { FlagGroup } from '@atlaskit/flag';
|
||||
import Portal from '@atlaskit/portal';
|
||||
import React from 'react';
|
||||
import { Transition, TransitionGroup } from 'react-transition-group';
|
||||
|
||||
import { connect } from '../../../base/redux';
|
||||
import AbstractNotificationsContainer, {
|
||||
@@ -27,6 +28,16 @@ type Props = AbstractProps & {
|
||||
* @extends {Component}
|
||||
*/
|
||||
class NotificationsContainer extends AbstractNotificationsContainer<Props> {
|
||||
/**
|
||||
* Creates new NotificationContainer instance.
|
||||
*
|
||||
* @param {Props} props - The props of the react component.
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this._renderNotification = this._renderNotification.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
@@ -40,39 +51,92 @@ class NotificationsContainer extends AbstractNotificationsContainer<Props> {
|
||||
}
|
||||
|
||||
return (
|
||||
<FlagGroup onDismissed = { this._onDismissed }>
|
||||
{ this._renderFlags() }
|
||||
</FlagGroup>
|
||||
<Portal zIndex = { 600 }>
|
||||
<div className = 'notificationsContainer'>
|
||||
{ this._renderTopNotificationsContainer() }
|
||||
{ this._renderBottomNotificationsContainer() }
|
||||
</div>
|
||||
</Portal>
|
||||
);
|
||||
}
|
||||
|
||||
_onDismissed: number => void;
|
||||
|
||||
/**
|
||||
* Renders notifications to display as ReactElements. An empty array will
|
||||
* be returned if notifications are disabled.
|
||||
* Renders the bottom notification container.
|
||||
*
|
||||
* @private
|
||||
* @returns {ReactElement[]}
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderFlags() {
|
||||
_renderBottomNotificationsContainer() {
|
||||
const { _notifications } = this.props;
|
||||
|
||||
return _notifications.map(notification => {
|
||||
const { props, uid } = notification;
|
||||
return (
|
||||
<TransitionGroup className = 'bottomContainer'>
|
||||
{
|
||||
_notifications.filter(n => n.props.position !== 'top').map((notification, index) => {
|
||||
const { props, uid } = notification;
|
||||
|
||||
// The id attribute is necessary as {@code FlagGroup} looks for
|
||||
// either id or key to set a key on notifications, but accessing
|
||||
// props.key will cause React to print an error.
|
||||
return (
|
||||
<Notification
|
||||
{ ...props }
|
||||
id = { uid }
|
||||
key = { uid }
|
||||
uid = { uid } />
|
||||
return this._renderNotification({
|
||||
...props,
|
||||
isDismissAllowed: index > 0 ? false : props.isDismissAllowed
|
||||
}, uid);
|
||||
})
|
||||
}
|
||||
</TransitionGroup>
|
||||
);
|
||||
}
|
||||
|
||||
);
|
||||
});
|
||||
_renderNotification: (string, number) => Function;
|
||||
|
||||
/**
|
||||
* Renders a notification.
|
||||
*
|
||||
* @param {Object} props - The props for the Notification component.
|
||||
* @param {string} uid - A unique ID for the notification.
|
||||
* @returns {Function} - Returns a transition function for the Transition component.
|
||||
*/
|
||||
_renderNotification(props, uid) {
|
||||
return (
|
||||
<Transition
|
||||
key = { uid }
|
||||
timeout = { 400 }>
|
||||
{
|
||||
transitionState => (
|
||||
<div className = { `notification ${transitionState}` }>
|
||||
<Notification
|
||||
{ ...props }
|
||||
id = { uid }
|
||||
key = { uid }
|
||||
onDismissed = { this._onDismissed }
|
||||
uid = { uid } />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</Transition>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the top notifications container.
|
||||
*
|
||||
* @private
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderTopNotificationsContainer() {
|
||||
const { _notifications } = this.props;
|
||||
|
||||
return (
|
||||
<TransitionGroup className = 'topContainer'>
|
||||
{
|
||||
_notifications.filter(n => n.props.position === 'top').map(notification => {
|
||||
const { props, uid } = notification;
|
||||
|
||||
return this._renderNotification(props, uid);
|
||||
})
|
||||
}
|
||||
</TransitionGroup>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,11 +39,6 @@ export const SET_DIALOUT_STATUS = 'SET_DIALOUT_STATUS';
|
||||
*/
|
||||
export const SET_JOIN_BY_PHONE_DIALOG_VISIBLITY = 'SET_JOIN_BY_PHONE_DIALOG_VISIBLITY';
|
||||
|
||||
/**
|
||||
* Action type to set the precall test data.
|
||||
*/
|
||||
export const SET_PRECALL_TEST_RESULTS = 'SET_PRECALL_TEST_RESULTS';
|
||||
|
||||
/**
|
||||
* Action type to disable the audio while on prejoin page.
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
// @flow
|
||||
|
||||
declare var JitsiMeetJS: Object;
|
||||
|
||||
import uuid from 'uuid';
|
||||
|
||||
import { getRoomName } from '../base/conference';
|
||||
@@ -26,7 +24,6 @@ import {
|
||||
SET_PREJOIN_DISPLAY_NAME_REQUIRED,
|
||||
SET_SKIP_PREJOIN,
|
||||
SET_JOIN_BY_PHONE_DIALOG_VISIBLITY,
|
||||
SET_PRECALL_TEST_RESULTS,
|
||||
SET_PREJOIN_DEVICE_ERRORS,
|
||||
SET_PREJOIN_PAGE_VISIBILITY
|
||||
} from './actionTypes';
|
||||
@@ -204,13 +201,11 @@ export function initPrejoin(tracks: Object[], errors: Object) {
|
||||
/**
|
||||
* Action used to start the conference.
|
||||
*
|
||||
* @param {Object} options - The config options that override the default ones (if any).
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function joinConference(options?: Object) {
|
||||
export function joinConference() {
|
||||
return {
|
||||
type: PREJOIN_START_CONFERENCE,
|
||||
options
|
||||
type: PREJOIN_START_CONFERENCE
|
||||
};
|
||||
}
|
||||
|
||||
@@ -227,26 +222,7 @@ export function joinConferenceWithoutAudio() {
|
||||
if (audioTrack) {
|
||||
await dispatch(replaceLocalTrack(audioTrack, null));
|
||||
}
|
||||
|
||||
dispatch(joinConference({
|
||||
startSilent: true
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the 'precallTest' and executes one test, storing the results.
|
||||
*
|
||||
* @param {Object} conferenceOptions - The conference options.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function makePrecallTest(conferenceOptions: Object) {
|
||||
return async function(dispatch: Function) {
|
||||
await JitsiMeetJS.precallTest.init(conferenceOptions);
|
||||
|
||||
const results = await JitsiMeetJS.precallTest.execute();
|
||||
|
||||
dispatch(setPrecallTestResults(results));
|
||||
dispatch(joinConference());
|
||||
};
|
||||
}
|
||||
|
||||
@@ -416,19 +392,6 @@ export function setJoinByPhoneDialogVisiblity(value: boolean) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action used to set data from precall test.
|
||||
*
|
||||
* @param {Object} value - The precall test results.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function setPrecallTestResults(value: Object) {
|
||||
return {
|
||||
type: SET_PRECALL_TEST_RESULTS,
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action used to set the initial errors after creating the tracks.
|
||||
*
|
||||
|
||||
@@ -48,6 +48,11 @@ type Props = {
|
||||
*/
|
||||
hasJoinByPhoneButton: boolean,
|
||||
|
||||
/**
|
||||
* If join button is disabled or not.
|
||||
*/
|
||||
joinButtonDisabled: boolean,
|
||||
|
||||
/**
|
||||
* Joins the current meeting.
|
||||
*/
|
||||
@@ -93,11 +98,6 @@ type Props = {
|
||||
*/
|
||||
showCameraPreview: boolean,
|
||||
|
||||
/**
|
||||
* If should show an error when joining without a name.
|
||||
*/
|
||||
showErrorOnJoin: boolean,
|
||||
|
||||
/**
|
||||
* Flag signaling the visibility of join label, input and buttons
|
||||
*/
|
||||
@@ -131,11 +131,6 @@ type Props = {
|
||||
|
||||
type State = {
|
||||
|
||||
/**
|
||||
* Flag controlling the visibility of the error label.
|
||||
*/
|
||||
showError: boolean,
|
||||
|
||||
/**
|
||||
* Flag controlling the visibility of the 'join by phone' buttons.
|
||||
*/
|
||||
@@ -166,38 +161,16 @@ class Prejoin extends Component<Props, State> {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
showError: false,
|
||||
showJoinByPhoneButtons: false
|
||||
};
|
||||
|
||||
this._closeDialog = this._closeDialog.bind(this);
|
||||
this._showDialog = this._showDialog.bind(this);
|
||||
this._onJoinButtonClick = this._onJoinButtonClick.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);
|
||||
}
|
||||
_onJoinButtonClick: () => void;
|
||||
|
||||
/**
|
||||
* Handler for the join button.
|
||||
*
|
||||
* @param {Object} e - The synthetic event.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onJoinButtonClick() {
|
||||
if (this.props.showErrorOnJoin) {
|
||||
this.setState({
|
||||
showError: true
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ showError: false });
|
||||
this.props.joinConference();
|
||||
}
|
||||
|
||||
_onToggleButtonClick: () => void;
|
||||
|
||||
@@ -285,6 +258,7 @@ class Prejoin extends Component<Props, State> {
|
||||
*/
|
||||
render() {
|
||||
const {
|
||||
joinButtonDisabled,
|
||||
hasJoinByPhoneButton,
|
||||
joinConference,
|
||||
joinConferenceWithoutAudio,
|
||||
@@ -298,8 +272,8 @@ class Prejoin extends Component<Props, State> {
|
||||
videoTrack
|
||||
} = this.props;
|
||||
|
||||
const { _closeDialog, _onDropdownClose, _onJoinButtonClick, _onOptionsClick, _setName, _showDialog } = this;
|
||||
const { showJoinByPhoneButtons, showError } = this.state;
|
||||
const { _closeDialog, _onDropdownClose, _onOptionsClick, _setName, _showDialog } = this;
|
||||
const { showJoinByPhoneButtons } = this.state;
|
||||
|
||||
return (
|
||||
<PreMeetingScreen
|
||||
@@ -315,22 +289,16 @@ class Prejoin extends Component<Props, State> {
|
||||
<div className = 'prejoin-input-area-container'>
|
||||
<div className = 'prejoin-input-area'>
|
||||
<InputField
|
||||
autoFocus = { true }
|
||||
onChange = { _setName }
|
||||
onSubmit = { joinConference }
|
||||
placeHolder = { t('dialog.enterDisplayName') }
|
||||
value = { name } />
|
||||
|
||||
{showError && <div
|
||||
className = 'prejoin-error'
|
||||
data-testid = 'prejoin.errorMessage'>{t('prejoin.errorMissingName')}</div>}
|
||||
|
||||
<div className = 'prejoin-preview-dropdown-container'>
|
||||
<InlineDialog
|
||||
content = { <div className = 'prejoin-preview-dropdown-btns'>
|
||||
<div
|
||||
className = 'prejoin-preview-dropdown-btn'
|
||||
data-testid = 'prejoin.joinWithoutAudio'
|
||||
onClick = { joinConferenceWithoutAudio }>
|
||||
<Icon
|
||||
className = 'prejoin-preview-dropdown-icon'
|
||||
@@ -343,7 +311,6 @@ class Prejoin extends Component<Props, State> {
|
||||
onClick = { _showDialog }>
|
||||
<Icon
|
||||
className = 'prejoin-preview-dropdown-icon'
|
||||
data-testid = 'prejoin.joinByPhone'
|
||||
size = { 24 }
|
||||
src = { IconPhone } />
|
||||
{ t('prejoin.joinAudioByPhone') }
|
||||
@@ -352,8 +319,9 @@ class Prejoin extends Component<Props, State> {
|
||||
isOpen = { showJoinByPhoneButtons }
|
||||
onClose = { _onDropdownClose }>
|
||||
<ActionButton
|
||||
disabled = { joinButtonDisabled }
|
||||
hasOptions = { true }
|
||||
onClick = { _onJoinButtonClick }
|
||||
onClick = { joinConference }
|
||||
onOptionsClick = { _onOptionsClick }
|
||||
testId = 'prejoin.joinMeeting'
|
||||
type = 'primary'>
|
||||
@@ -415,7 +383,7 @@ class Prejoin extends Component<Props, State> {
|
||||
*/
|
||||
function mapStateToProps(state, ownProps): Object {
|
||||
const name = getDisplayName(state);
|
||||
const showErrorOnJoin = isDisplayNameRequired(state) && !name;
|
||||
const joinButtonDisabled = isDisplayNameRequired(state) && !name;
|
||||
const { showJoinActions } = ownProps;
|
||||
const isInviteButtonEnabled = isButtonEnabled('invite');
|
||||
|
||||
@@ -429,11 +397,11 @@ function mapStateToProps(state, ownProps): Object {
|
||||
|
||||
return {
|
||||
buttonIsToggled: isPrejoinSkipped(state),
|
||||
joinButtonDisabled,
|
||||
name,
|
||||
deviceStatusVisible: isDeviceStatusVisible(state),
|
||||
roomName: getRoomName(state),
|
||||
showDialog: isJoinByPhoneDialogVisible(state),
|
||||
showErrorOnJoin,
|
||||
hasJoinByPhoneButton: isJoinByPhoneButtonVisible(state),
|
||||
showCameraPreview: !isVideoMutedByUser(state),
|
||||
showConferenceInfo,
|
||||
|
||||
@@ -1,22 +1,12 @@
|
||||
// @flow
|
||||
|
||||
import { updateConfig } from '../base/config';
|
||||
import { SET_AUDIO_MUTED, SET_VIDEO_MUTED } from '../base/media';
|
||||
import { MiddlewareRegistry } from '../base/redux';
|
||||
import { updateSettings } from '../base/settings';
|
||||
import {
|
||||
getLocalVideoTrack,
|
||||
replaceLocalTrack,
|
||||
TRACK_ADDED,
|
||||
TRACK_NO_DATA_FROM_SOURCE
|
||||
} from '../base/tracks';
|
||||
import { getLocalVideoTrack, replaceLocalTrack } from '../base/tracks';
|
||||
|
||||
import { PREJOIN_START_CONFERENCE } from './actionTypes';
|
||||
import {
|
||||
setDeviceStatusOk,
|
||||
setDeviceStatusWarning,
|
||||
setPrejoinPageVisibility
|
||||
} from './actions';
|
||||
import { setPrejoinPageVisibility } from './actions';
|
||||
import { isPrejoinPageVisible } from './functions';
|
||||
|
||||
declare var APP: Object;
|
||||
@@ -34,9 +24,6 @@ MiddlewareRegistry.register(store => next => async action => {
|
||||
const state = getState();
|
||||
const { userSelectedSkipPrejoin } = state['features/prejoin'];
|
||||
const localVideoTrack = getLocalVideoTrack(state['features/base/tracks']);
|
||||
const { options } = action;
|
||||
|
||||
options && store.dispatch(updateConfig(options));
|
||||
|
||||
userSelectedSkipPrejoin && dispatch(updateSettings({
|
||||
userSelectedSkipPrejoin
|
||||
@@ -72,30 +59,6 @@ MiddlewareRegistry.register(store => next => async action => {
|
||||
break;
|
||||
}
|
||||
|
||||
case TRACK_ADDED:
|
||||
case TRACK_NO_DATA_FROM_SOURCE: {
|
||||
const state = store.getState();
|
||||
|
||||
if (isPrejoinPageVisible(state)) {
|
||||
const { track: { jitsiTrack: track } } = action;
|
||||
const { deviceStatusType, deviceStatusText } = state['features/prejoin'];
|
||||
|
||||
if (!track.isAudioTrack()) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (track.isReceivingData()) {
|
||||
if (deviceStatusType === 'warning'
|
||||
&& deviceStatusText === 'prejoin.audioDeviceProblem') {
|
||||
store.dispatch(setDeviceStatusOk('prejoin.lookGood'));
|
||||
}
|
||||
} else if (deviceStatusType === 'ok') {
|
||||
store.dispatch(setDeviceStatusWarning('prejoin.audioDeviceProblem'));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return next(action);
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
SET_DIALOUT_NUMBER,
|
||||
SET_DIALOUT_STATUS,
|
||||
SET_JOIN_BY_PHONE_DIALOG_VISIBLITY,
|
||||
SET_PRECALL_TEST_RESULTS,
|
||||
SET_PREJOIN_DEVICE_ERRORS,
|
||||
SET_PREJOIN_DISPLAY_NAME_REQUIRED,
|
||||
SET_PREJOIN_PAGE_VISIBILITY,
|
||||
@@ -46,12 +45,6 @@ ReducerRegistry.register(
|
||||
};
|
||||
}
|
||||
|
||||
case SET_PRECALL_TEST_RESULTS:
|
||||
return {
|
||||
...state,
|
||||
precallTestResults: action.value
|
||||
};
|
||||
|
||||
case SET_PREJOIN_PAGE_VISIBILITY:
|
||||
return {
|
||||
...state,
|
||||
@@ -68,12 +61,10 @@ ReducerRegistry.register(
|
||||
}
|
||||
|
||||
case SET_DEVICE_STATUS: {
|
||||
const { deviceStatusType, deviceStatusText } = action.value;
|
||||
|
||||
return {
|
||||
...state,
|
||||
deviceStatusText,
|
||||
deviceStatusType
|
||||
deviceStatusText: action.text,
|
||||
deviceStatusType: action.type
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -46,3 +46,17 @@ export const SET_PENDING_RECORDING_NOTIFICATION_UID
|
||||
* }
|
||||
*/
|
||||
export const SET_STREAM_KEY = 'SET_STREAM_KEY';
|
||||
|
||||
/**
|
||||
* The type of Redux action which sets the waiting in queue recording notification UID to
|
||||
* use it for when hiding the notification is necessary, or unsets it when
|
||||
* undefined (or no param) is passed.
|
||||
*
|
||||
* {
|
||||
* type: SET_WAITING_IN_RECORDING_NOTIFICATION_UID,
|
||||
* streamType: string,
|
||||
* uid: ?number
|
||||
* }
|
||||
* @public
|
||||
*/
|
||||
export const SET_WAITING_IN_RECORDING_NOTIFICATION_UID = 'SET_WAITING_IN_RECORDING_NOTIFICATION_UID';
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { openDialog } from '../base/dialog';
|
||||
import JitsiMeetJS, { JitsiRecordingConstants } from '../base/lib-jitsi-meet';
|
||||
import {
|
||||
NOTIFICATION_TIMEOUT,
|
||||
NOTIFICATION_TYPE,
|
||||
hideNotification,
|
||||
showErrorNotification,
|
||||
showNotification
|
||||
@@ -12,8 +16,10 @@ import {
|
||||
CLEAR_RECORDING_SESSIONS,
|
||||
RECORDING_SESSION_UPDATED,
|
||||
SET_PENDING_RECORDING_NOTIFICATION_UID,
|
||||
SET_STREAM_KEY
|
||||
SET_STREAM_KEY,
|
||||
SET_WAITING_IN_RECORDING_NOTIFICATION_UID
|
||||
} from './actionTypes';
|
||||
import { QueueInfo, StopLiveStreamDialog, StopRecordingDialog } from './components';
|
||||
|
||||
/**
|
||||
* Clears the data of every recording sessions.
|
||||
@@ -50,6 +56,25 @@ export function hidePendingRecordingNotification(streamType: string) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals that the waiting in queue recording notification should be removed from the screen.
|
||||
*
|
||||
* @param {string} streamType - The type of the stream ({@code 'file'} or
|
||||
* {@code 'stream'}).
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function hideWaitingInQueueRecordingNotification(streamType: string) {
|
||||
return (dispatch: Function, getState: Function) => {
|
||||
const { waitingInQueueNotificationUids } = getState()['features/recording'];
|
||||
const waitingInQueueNotificationUid = waitingInQueueNotificationUids[streamType];
|
||||
|
||||
if (waitingInQueueNotificationUid) {
|
||||
dispatch(hideNotification(waitingInQueueNotificationUid));
|
||||
dispatch(_setWaitingInQueueRecordingNotificationUid(undefined, streamType));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the stream key last used by the user for later reuse.
|
||||
*
|
||||
@@ -97,6 +122,22 @@ export function showPendingRecordingNotification(streamType: string) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals that the jibri queue has been left and notification should be shown on the
|
||||
* screen.
|
||||
*
|
||||
* @param {string} streamType - The type of the stream ({@code file} or
|
||||
* {@code stream}).
|
||||
* @returns {showNotification}
|
||||
*/
|
||||
export function showQueueLeftRecordingNotification(streamType: string) {
|
||||
const isLiveStreaming = streamType === JitsiMeetJS.constants.recording.mode.STREAM;
|
||||
|
||||
return showNotification({
|
||||
titleKey: `jibriQueue.${isLiveStreaming ? 'livestreaming' : 'recording'}.left`
|
||||
}, NOTIFICATION_TIMEOUT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals that the recording error notification should be shown.
|
||||
*
|
||||
@@ -175,6 +216,15 @@ export function updateRecordingSessionData(session: Object) {
|
||||
= status === JitsiRecordingConstants.status.ON
|
||||
? Date.now() / 1000
|
||||
: undefined;
|
||||
const queueID = session.getQueueID();
|
||||
let queueEstimatedTimeOfStart, queuePosition;
|
||||
|
||||
if (status === JitsiRecordingConstants.status.WAITING_IN_QUEUE) {
|
||||
const { position, estimatedTimeLeft } = session.getQueueMetrics();
|
||||
|
||||
queuePosition = position;
|
||||
queueEstimatedTimeOfStart = (new Date()).getTime() + (estimatedTimeLeft * 1000);
|
||||
}
|
||||
|
||||
return {
|
||||
type: RECORDING_SESSION_UPDATED,
|
||||
@@ -186,7 +236,10 @@ export function updateRecordingSessionData(session: Object) {
|
||||
mode: session.getMode(),
|
||||
status,
|
||||
terminator: session.getTerminator(),
|
||||
timestamp
|
||||
timestamp,
|
||||
queueID,
|
||||
queuePosition,
|
||||
queueEstimatedTimeOfStart
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -212,3 +265,57 @@ function _setPendingRecordingNotificationUid(uid: ?number, streamType: string) {
|
||||
uid
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets UID of the the pending streaming notification to use it when hiding
|
||||
* the notification is necessary, or unsets it when undefined (or no param) is
|
||||
* passed.
|
||||
*
|
||||
* @param {?number} uid - The UID of the notification.
|
||||
* @param {string} streamType - The type of the stream ({@code file} or {@code stream}).
|
||||
* @returns {{
|
||||
* type: SET_PENDING_RECORDING_NOTIFICATION_UID,
|
||||
* streamType: string,
|
||||
* uid: number
|
||||
* }}
|
||||
*/
|
||||
function _setWaitingInQueueRecordingNotificationUid(uid: ?number, streamType: string) {
|
||||
return {
|
||||
type: SET_WAITING_IN_RECORDING_NOTIFICATION_UID,
|
||||
streamType,
|
||||
uid
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals that the recording queue notification should be shown on the screen.
|
||||
*
|
||||
* @param {string} streamType - The type of the stream ({@code file} or
|
||||
* {@code stream}).
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function showWaitingInQueueRecordingNotification(streamType: string) {
|
||||
return (dispatch: Function) => {
|
||||
const isLiveStreaming = streamType === JitsiMeetJS.constants.recording.mode.STREAM;
|
||||
const showNotificationAction = showNotification({
|
||||
appearance: NOTIFICATION_TYPE.INFO,
|
||||
customActionNameKey: 'jibriQueue.exit',
|
||||
customActionHandler: () => {
|
||||
if (isLiveStreaming) {
|
||||
dispatch(openDialog(StopLiveStreamDialog));
|
||||
} else {
|
||||
dispatch(openDialog(StopRecordingDialog));
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
position: 'top',
|
||||
titleKey: `jibriQueue.${isLiveStreaming ? 'livestreaming' : 'recording'}.title`,
|
||||
description: <QueueInfo />
|
||||
});
|
||||
|
||||
dispatch(showNotificationAction);
|
||||
dispatch(_setWaitingInQueueRecordingNotificationUid(
|
||||
showNotificationAction.uid, streamType));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -217,6 +217,8 @@ export default class AbstractStartLiveStreamDialog<P: Props>
|
||||
broadcastId: selectedBroadcastID,
|
||||
mode: JitsiRecordingConstants.mode.STREAM,
|
||||
streamId: key
|
||||
}).catch(() => {
|
||||
// prevent unhandled promise rejection.
|
||||
});
|
||||
|
||||
return true;
|
||||
|
||||
@@ -65,7 +65,9 @@ export default class AbstractStopLiveStreamDialog extends Component<Props> {
|
||||
const { _session } = this.props;
|
||||
|
||||
if (_session) {
|
||||
this.props._conference.stopRecording(_session.id);
|
||||
this.props._conference.stopRecording(_session.id, _session.queueID).catch(() => {
|
||||
// prevent unhandled promise rejection.
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -4,6 +4,7 @@ import React from 'react';
|
||||
|
||||
import { Dialog } from '../../../../base/dialog';
|
||||
import { translate } from '../../../../base/i18n';
|
||||
import { JitsiRecordingConstants } from '../../../../base/lib-jitsi-meet';
|
||||
import { connect } from '../../../../base/redux';
|
||||
import AbstractStopLiveStreamDialog, {
|
||||
_mapStateToProps
|
||||
@@ -24,13 +25,17 @@ class StopLiveStreamDialog extends AbstractStopLiveStreamDialog {
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { _session = {}, t } = this.props;
|
||||
|
||||
const isInQueue = _session.status === JitsiRecordingConstants.status.WAITING_IN_QUEUE;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
okKey = 'dialog.stopLiveStreaming'
|
||||
okKey = { isInQueue ? 'dialog.leaveJibriQueue' : 'dialog.stopLiveStreaming' }
|
||||
onSubmit = { this._onSubmit }
|
||||
titleKey = 'dialog.liveStreaming'
|
||||
width = 'small'>
|
||||
{ this.props.t('dialog.stopStreamingWarning') }
|
||||
{ t(isInQueue ? 'dialog.leaveJibriQueueWarning' : 'dialog.stopStreamingWarning') }
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -280,6 +280,8 @@ class AbstractStartRecordingDialog extends Component<Props, State> {
|
||||
_conference.startRecording({
|
||||
mode: JitsiRecordingConstants.mode.FILE,
|
||||
appData
|
||||
}).catch(() => {
|
||||
// prevent unhandled promise rejection.
|
||||
});
|
||||
|
||||
if (_autoCaptionOnRecord) {
|
||||
|
||||
@@ -65,7 +65,9 @@ export default class AbstractStopRecordingDialog<P: Props>
|
||||
const { _fileRecordingSession } = this.props;
|
||||
|
||||
if (_fileRecordingSession) {
|
||||
this.props._conference.stopRecording(_fileRecordingSession.id);
|
||||
this.props._conference.stopRecording(_fileRecordingSession.id, _fileRecordingSession.queueID).catch(() => {
|
||||
// prevent unhandled promise rejection.
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
} from '../../../base/react';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { ColorPalette, StyleType } from '../../../base/styles';
|
||||
import { isVpaasMeeting } from '../../../billing-counter/functions';
|
||||
import { authorizeDropbox, updateDropboxToken } from '../../../dropbox';
|
||||
import { RECORDING_TYPES } from '../../constants';
|
||||
import { getRecordingDurationEstimation } from '../../functions';
|
||||
@@ -73,11 +72,6 @@ type Props = {
|
||||
*/
|
||||
isValidating: boolean,
|
||||
|
||||
/**
|
||||
* Whether or not the current meeting is a vpaas one.
|
||||
*/
|
||||
isVpaas: boolean,
|
||||
|
||||
/**
|
||||
* The function will be called when there are changes related to the
|
||||
* switches.
|
||||
@@ -232,7 +226,7 @@ class StartRecordingDialogContent extends Component<Props> {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { _dialogStyles, _styles: styles, isValidating, isVpaas, t } = this.props;
|
||||
const { _dialogStyles, _styles: styles, isValidating, t } = this.props;
|
||||
|
||||
const switchContent
|
||||
= this.props.integrationsEnabled
|
||||
@@ -246,8 +240,6 @@ class StartRecordingDialogContent extends Component<Props> {
|
||||
value = { this.props.selectedRecordingService === RECORDING_TYPES.JITSI_REC_SERVICE } />
|
||||
) : null;
|
||||
|
||||
const icon = isVpaas ? ICON_SHARE : JITSI_LOGO;
|
||||
|
||||
return (
|
||||
<Container
|
||||
className = 'recording-header'
|
||||
@@ -256,7 +248,7 @@ class StartRecordingDialogContent extends Component<Props> {
|
||||
<Container className = 'recording-icon-container'>
|
||||
<Image
|
||||
className = 'recording-icon'
|
||||
src = { icon }
|
||||
src = { JITSI_LOGO }
|
||||
style = { styles.recordingIcon } />
|
||||
</Container>
|
||||
<Text
|
||||
@@ -492,7 +484,6 @@ class StartRecordingDialogContent extends Component<Props> {
|
||||
function _mapStateToProps(state) {
|
||||
return {
|
||||
..._abstractMapStateToProps(state),
|
||||
isVpaas: isVpaasMeeting(state),
|
||||
_styles: ColorSchemeRegistry.get(state, 'StartRecordingDialogContent')
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import React from 'react';
|
||||
|
||||
import { Dialog } from '../../../../base/dialog';
|
||||
import { translate } from '../../../../base/i18n';
|
||||
import { JitsiRecordingConstants } from '../../../../base/lib-jitsi-meet';
|
||||
import { connect } from '../../../../base/redux';
|
||||
import AbstractStopRecordingDialog, {
|
||||
type Props,
|
||||
@@ -24,15 +25,17 @@ class StopRecordingDialog extends AbstractStopRecordingDialog<Props> {
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
const { _fileRecordingSession = {}, t } = this.props;
|
||||
|
||||
const isInQueue = _fileRecordingSession.status === JitsiRecordingConstants.status.WAITING_IN_QUEUE;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
okKey = 'dialog.confirm'
|
||||
okKey = { isInQueue ? 'dialog.leaveJibriQueue' : 'dialog.confirm' }
|
||||
onSubmit = { this._onSubmit }
|
||||
titleKey = 'dialog.recording'
|
||||
width = 'small'>
|
||||
{ t('dialog.stopRecordingWarning') }
|
||||
{ t(isInQueue ? 'dialog.leaveJibriQueueWarning' : 'dialog.stopRecordingWarning') }
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
230
react/features/recording/components/web/QueueInfo.js
Normal file
230
react/features/recording/components/web/QueueInfo.js
Normal file
@@ -0,0 +1,230 @@
|
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { getLocalizedDurationFormatter, translate } from '../../../base/i18n';
|
||||
import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { getActiveSession } from '../../functions';
|
||||
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The current position of the participant in the queue.
|
||||
*/
|
||||
_position: ?string,
|
||||
|
||||
/**
|
||||
* The recording mode.
|
||||
*/
|
||||
_mode: string,
|
||||
|
||||
/**
|
||||
* The ID of the queue.
|
||||
*/
|
||||
_queueID: string,
|
||||
|
||||
/**
|
||||
* The time when the recording is expected to start.
|
||||
*/
|
||||
_estimatedTimeOfStart: number,
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
t: Function
|
||||
};
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} state of {@link QueueInfo}.
|
||||
*/
|
||||
type State = {
|
||||
|
||||
/**
|
||||
* The current value of the timer for estimated time left.
|
||||
*/
|
||||
timerValue: ?string
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements a React {@link Component} which displays the current state of the Jibri Queue.
|
||||
*
|
||||
* @extends {Component}
|
||||
*/
|
||||
class QueueInfo extends Component<Props, State> {
|
||||
|
||||
/**
|
||||
* Handle for setInterval timer.
|
||||
*/
|
||||
_interval: IntervalID;
|
||||
|
||||
/**
|
||||
* Initializes a new {@code QueueInfo} instance.
|
||||
*
|
||||
* @param {Props} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
timerValue: undefined
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the timer when component will be unmounted.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
this._stopTimer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the timer when component will be mounted.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentDidMount() {
|
||||
if (typeof this.props._estimatedTimeOfStart !== 'undefined') {
|
||||
this._startTimer();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React Component's componentDidUpdate.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props._estimatedTimeOfStart !== prevProps._estimatedTimeOfStart) {
|
||||
this._stopTimer(false);
|
||||
this._startTimer();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current state values that will be used to render the timer.
|
||||
*
|
||||
* @param {number} refValueUTC - The initial UTC timestamp value.
|
||||
* @param {number} currentValueUTC - The current UTC timestamp value.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_setStateFromUTC(refValueUTC, currentValueUTC) {
|
||||
if (!refValueUTC || !currentValueUTC) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timerMsValue = currentValueUTC > refValueUTC ? 0 : refValueUTC - currentValueUTC;
|
||||
const localizedTime = getLocalizedDurationFormatter(timerMsValue);
|
||||
|
||||
this.setState({
|
||||
timerValue: localizedTime
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the timer.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_startTimer() {
|
||||
const { _estimatedTimeOfStart } = this.props;
|
||||
|
||||
if (!this._interval && typeof _estimatedTimeOfStart !== 'undefined') {
|
||||
this._setStateFromUTC(_estimatedTimeOfStart, (new Date()).getTime());
|
||||
this._interval = setInterval(() => {
|
||||
this._setStateFromUTC(_estimatedTimeOfStart, (new Date()).getTime());
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the timer.
|
||||
*
|
||||
* @param {boolean} [clearState] - If true, the timer value in the state will be cleared.
|
||||
* @returns {void}
|
||||
*/
|
||||
_stopTimer(clearState = true) {
|
||||
if (this._interval) {
|
||||
clearInterval(this._interval);
|
||||
delete this._interval;
|
||||
}
|
||||
|
||||
if (clearState) {
|
||||
this.setState({
|
||||
timerValue: undefined
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React {@code Component}'s render.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const { _estimatedTimeOfStart, _mode, _position = 0, t } = this.props;
|
||||
const { STREAM } = JitsiRecordingConstants.mode;
|
||||
const timeTextKey = `jibriQueue.${_mode === STREAM ? 'livestreaming' : 'recording'}.time`;
|
||||
const { timerValue } = this.state;
|
||||
const footerText = t(`jibriQueue.${_mode === STREAM ? 'livestreaming' : 'recording'}.footer`);
|
||||
const showFooter = typeof footerText === 'string' && footerText.length > 0;
|
||||
|
||||
|
||||
return (
|
||||
<div className = 'jibri-queue-info'>
|
||||
<span className = 'position'>
|
||||
{ t('jibriQueue.position', { count: _position }) }
|
||||
</span>
|
||||
{
|
||||
typeof _estimatedTimeOfStart === 'undefined' || timerValue === 'undefined'
|
||||
? null : <span className = 'time'>
|
||||
{ t(timeTextKey, { time: timerValue }) }
|
||||
</span>
|
||||
}
|
||||
{
|
||||
showFooter ? <div className = 'footer'>{ footerText }</div> : null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated
|
||||
* {@code AbstractRecordingLabel}'s props.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _estimatedTimeOfStart: number,
|
||||
* _mode: string,
|
||||
* _position: string,
|
||||
* _queueID: string,
|
||||
* t: Function
|
||||
* }}
|
||||
*/
|
||||
export function _mapStateToProps(state: Object) {
|
||||
const session = getActiveSession(state);
|
||||
|
||||
if (!session) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const { id, mode, queueEstimatedTimeOfStart, queueID, queuePosition } = session;
|
||||
|
||||
return {
|
||||
_sessionID: id,
|
||||
_mode: mode,
|
||||
_queueID: queueID,
|
||||
_position: queuePosition,
|
||||
_estimatedTimeOfStart: queueEstimatedTimeOfStart
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(QueueInfo));
|
||||
@@ -2,3 +2,4 @@
|
||||
|
||||
export { default as RecordingLabel } from './RecordingLabel';
|
||||
export { default as RecordingLimitNotificationDescription } from './RecordingLimitNotificationDescription';
|
||||
export { default as QueueInfo } from './QueueInfo';
|
||||
|
||||
@@ -9,16 +9,17 @@ import { RECORDING_STATUS_PRIORITIES } from './constants';
|
||||
* passed in mode.
|
||||
*
|
||||
* @param {Object} state - The redux state to search in.
|
||||
* @param {string} mode - Find an active recording session of the given mode.
|
||||
* @param {string|undefined} mode - Find an active recording session of the given mode.
|
||||
* @returns {Object|undefined}
|
||||
*/
|
||||
export function getActiveSession(state: Object, mode: string) {
|
||||
export function getActiveSession(state: Object, mode: ?string) {
|
||||
const { sessionDatas } = state['features/recording'];
|
||||
const { status: statusConstants } = JitsiRecordingConstants;
|
||||
|
||||
return sessionDatas.find(sessionData => sessionData.mode === mode
|
||||
return sessionDatas.find(sessionData => (typeof mode === 'undefined' || sessionData.mode === mode)
|
||||
&& (sessionData.status === statusConstants.ON
|
||||
|| sessionData.status === statusConstants.PENDING));
|
||||
|| sessionData.status === statusConstants.PENDING
|
||||
|| sessionData.status === statusConstants.WAITING_IN_QUEUE));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -37,6 +38,8 @@ export function getRecordingDurationEstimation(size: ?number) {
|
||||
* Searches in the passed in redux state for a recording session that matches
|
||||
* the passed in recording session ID.
|
||||
*
|
||||
* NOTE: The sessoins in WAITING_IN_QUEUE status don't have ID yet.
|
||||
*
|
||||
* @param {Object} state - The redux state to search in.
|
||||
* @param {string} id - The ID of the recording session to find.
|
||||
* @returns {Object|undefined}
|
||||
@@ -51,6 +54,8 @@ export function getSessionById(state: Object, id: string) {
|
||||
* there is a session with the status OFF and one with PENDING, then the PENDING
|
||||
* one will be shown, because that is likely more important for the user to see.
|
||||
*
|
||||
* NOTE: For all "queue" statuses the function returns undefined because we don't want to show label.
|
||||
*
|
||||
* @param {Object} state - The redux state to search in.
|
||||
* @param {string} mode - The recording mode to get status for.
|
||||
* @returns {string|undefined}
|
||||
|
||||
@@ -24,11 +24,14 @@ import { RECORDING_SESSION_UPDATED } from './actionTypes';
|
||||
import {
|
||||
clearRecordingSessions,
|
||||
hidePendingRecordingNotification,
|
||||
hideWaitingInQueueRecordingNotification,
|
||||
showPendingRecordingNotification,
|
||||
showQueueLeftRecordingNotification,
|
||||
showRecordingError,
|
||||
showRecordingLimitNotification,
|
||||
showStartedRecordingNotification,
|
||||
showStoppedRecordingNotification,
|
||||
showWaitingInQueueRecordingNotification,
|
||||
updateRecordingSessionData
|
||||
} from './actions';
|
||||
import {
|
||||
@@ -110,15 +113,16 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
conference.on(
|
||||
JitsiConferenceEvents.RECORDER_STATE_CHANGED,
|
||||
recorderSession => {
|
||||
|
||||
if (recorderSession) {
|
||||
recorderSession.getID()
|
||||
&& dispatch(
|
||||
if (recorderSession.getID() || recorderSession.getQueueID()) {
|
||||
dispatch(
|
||||
updateRecordingSessionData(recorderSession));
|
||||
}
|
||||
|
||||
recorderSession.getError()
|
||||
&& _showRecordingErrorNotification(
|
||||
if (recorderSession.getError()) {
|
||||
_showRecordingErrorNotification(
|
||||
recorderSession, dispatch);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -142,75 +146,91 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
break;
|
||||
}
|
||||
|
||||
const updatedSessionData
|
||||
= getSessionById(getState(), action.sessionData.id);
|
||||
const { initiator, mode, terminator } = updatedSessionData;
|
||||
const { PENDING, OFF, ON } = JitsiRecordingConstants.status;
|
||||
const updatedSessionData = getSessionById(getState(), action.sessionData.id);
|
||||
const { initiator, mode, status: newStatus, terminator } = updatedSessionData;
|
||||
const { PENDING, OFF, ON, WAITING_IN_QUEUE, QUEUE_LEFT } = JitsiRecordingConstants.status;
|
||||
|
||||
if (updatedSessionData.status === PENDING
|
||||
&& (!oldSessionData || oldSessionData.status !== PENDING)) {
|
||||
dispatch(showPendingRecordingNotification(mode));
|
||||
} else if (updatedSessionData.status !== PENDING) {
|
||||
if (oldSessionData && oldSessionData.status === newStatus) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (newStatus !== WAITING_IN_QUEUE) {
|
||||
dispatch(hideWaitingInQueueRecordingNotification(mode));
|
||||
}
|
||||
|
||||
if (newStatus !== PENDING) {
|
||||
dispatch(hidePendingRecordingNotification(mode));
|
||||
}
|
||||
|
||||
if (updatedSessionData.status === ON
|
||||
&& (!oldSessionData || oldSessionData.status !== ON)) {
|
||||
if (initiator) {
|
||||
const initiatorName = initiator && getParticipantDisplayName(getState, initiator.getId());
|
||||
switch (newStatus) {
|
||||
case WAITING_IN_QUEUE:
|
||||
dispatch(showWaitingInQueueRecordingNotification(mode));
|
||||
break;
|
||||
case QUEUE_LEFT:
|
||||
dispatch(showQueueLeftRecordingNotification(mode));
|
||||
break;
|
||||
case PENDING:
|
||||
dispatch(showPendingRecordingNotification(mode));
|
||||
break;
|
||||
case ON: {
|
||||
if (initiator) {
|
||||
const initiatorName = initiator && getParticipantDisplayName(getState, initiator.getId());
|
||||
|
||||
initiatorName && dispatch(showStartedRecordingNotification(mode, initiatorName));
|
||||
} else if (typeof recordingLimit === 'object') {
|
||||
// Show notification with additional information to the initiator.
|
||||
dispatch(showRecordingLimitNotification(mode));
|
||||
}
|
||||
|
||||
|
||||
sendAnalytics(createRecordingEvent('start', mode));
|
||||
|
||||
if (disableRecordAudioNotification) {
|
||||
break;
|
||||
}
|
||||
|
||||
let soundID;
|
||||
|
||||
if (mode === JitsiRecordingConstants.mode.FILE) {
|
||||
soundID = RECORDING_ON_SOUND_ID;
|
||||
} else if (mode === JitsiRecordingConstants.mode.STREAM) {
|
||||
soundID = LIVE_STREAMING_ON_SOUND_ID;
|
||||
}
|
||||
|
||||
if (soundID) {
|
||||
dispatch(playSound(soundID));
|
||||
}
|
||||
} else if (updatedSessionData.status === OFF
|
||||
&& (!oldSessionData || oldSessionData.status !== OFF)) {
|
||||
dispatch(showStoppedRecordingNotification(
|
||||
mode, terminator && getParticipantDisplayName(getState, terminator.getId())));
|
||||
let duration = 0, soundOff, soundOn;
|
||||
|
||||
if (oldSessionData && oldSessionData.timestamp) {
|
||||
duration
|
||||
= (Date.now() / 1000) - oldSessionData.timestamp;
|
||||
}
|
||||
sendAnalytics(createRecordingEvent('stop', mode, duration));
|
||||
|
||||
if (disableRecordAudioNotification) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (mode === JitsiRecordingConstants.mode.FILE) {
|
||||
soundOff = RECORDING_OFF_SOUND_ID;
|
||||
soundOn = RECORDING_ON_SOUND_ID;
|
||||
} else if (mode === JitsiRecordingConstants.mode.STREAM) {
|
||||
soundOff = LIVE_STREAMING_OFF_SOUND_ID;
|
||||
soundOn = LIVE_STREAMING_ON_SOUND_ID;
|
||||
}
|
||||
|
||||
if (soundOff && soundOn) {
|
||||
dispatch(stopSound(soundOn));
|
||||
dispatch(playSound(soundOff));
|
||||
}
|
||||
initiatorName && dispatch(showStartedRecordingNotification(mode, initiatorName));
|
||||
} else if (typeof recordingLimit === 'object') {
|
||||
// Show notification with additional information to the initiator.
|
||||
dispatch(showRecordingLimitNotification(mode));
|
||||
}
|
||||
|
||||
|
||||
sendAnalytics(createRecordingEvent('start', mode));
|
||||
|
||||
if (disableRecordAudioNotification) {
|
||||
return result;
|
||||
}
|
||||
|
||||
let soundID;
|
||||
|
||||
if (mode === JitsiRecordingConstants.mode.FILE) {
|
||||
soundID = RECORDING_ON_SOUND_ID;
|
||||
} else if (mode === JitsiRecordingConstants.mode.STREAM) {
|
||||
soundID = LIVE_STREAMING_ON_SOUND_ID;
|
||||
}
|
||||
|
||||
if (soundID) {
|
||||
dispatch(playSound(soundID));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case OFF: {
|
||||
dispatch(showStoppedRecordingNotification(
|
||||
mode, terminator && getParticipantDisplayName(getState, terminator.getId())));
|
||||
let duration = 0, soundOff, soundOn;
|
||||
|
||||
if (oldSessionData && oldSessionData.timestamp) {
|
||||
duration
|
||||
= (Date.now() / 1000) - oldSessionData.timestamp;
|
||||
}
|
||||
sendAnalytics(createRecordingEvent('stop', mode, duration));
|
||||
|
||||
if (disableRecordAudioNotification) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (mode === JitsiRecordingConstants.mode.FILE) {
|
||||
soundOff = RECORDING_OFF_SOUND_ID;
|
||||
soundOn = RECORDING_ON_SOUND_ID;
|
||||
} else if (mode === JitsiRecordingConstants.mode.STREAM) {
|
||||
soundOff = LIVE_STREAMING_OFF_SOUND_ID;
|
||||
soundOn = LIVE_STREAMING_ON_SOUND_ID;
|
||||
}
|
||||
|
||||
if (soundOff && soundOn) {
|
||||
dispatch(stopSound(soundOn));
|
||||
dispatch(playSound(soundOff));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
@@ -4,12 +4,14 @@ import {
|
||||
CLEAR_RECORDING_SESSIONS,
|
||||
RECORDING_SESSION_UPDATED,
|
||||
SET_PENDING_RECORDING_NOTIFICATION_UID,
|
||||
SET_STREAM_KEY
|
||||
SET_STREAM_KEY,
|
||||
SET_WAITING_IN_RECORDING_NOTIFICATION_UID
|
||||
} from './actionTypes';
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
pendingNotificationUids: {},
|
||||
sessionDatas: []
|
||||
sessionDatas: [],
|
||||
waitingInQueueNotificationUids: {}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -56,6 +58,20 @@ ReducerRegistry.register(STORE_NAME,
|
||||
streamKey: action.streamKey
|
||||
};
|
||||
|
||||
case SET_WAITING_IN_RECORDING_NOTIFICATION_UID: {
|
||||
const waitingInQueueNotificationUids = {
|
||||
...state.waitingInQueueNotificationUids
|
||||
};
|
||||
|
||||
waitingInQueueNotificationUids[action.streamType] = action.uid;
|
||||
|
||||
return {
|
||||
...state,
|
||||
waitingInQueueNotificationUids
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
@@ -71,12 +87,12 @@ ReducerRegistry.register(STORE_NAME,
|
||||
*/
|
||||
function _updateSessionDatas(sessionDatas, newSessionData) {
|
||||
const hasExistingSessionData = sessionDatas.find(
|
||||
sessionData => sessionData.id === newSessionData.id);
|
||||
sessionData => sessionData.id === newSessionData.id || sessionData.queueID === newSessionData.queueID);
|
||||
let newSessionDatas;
|
||||
|
||||
if (hasExistingSessionData) {
|
||||
newSessionDatas = sessionDatas.map(sessionData => {
|
||||
if (sessionData.id === newSessionData.id) {
|
||||
if (sessionData.id === newSessionData.id || sessionData.queueID === newSessionData.queueID) {
|
||||
return {
|
||||
...newSessionData
|
||||
};
|
||||
|
||||
@@ -25,7 +25,7 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
|
||||
switch (action.type) {
|
||||
case LIB_WILL_INIT: {
|
||||
if (analytics.rtcstatsEnabled) {
|
||||
if (analytics.rtcstatsEndpoint) {
|
||||
// RTCStats "proxies" WebRTC functions such as GUM and RTCPeerConnection by rewriting the global
|
||||
// window functions. Because lib-jitsi-meet uses references to those functions that are taken on
|
||||
// init, we need to add these proxies before it initializes, otherwise lib-jitsi-meet will use the
|
||||
@@ -47,7 +47,7 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
break;
|
||||
}
|
||||
case CONFERENCE_JOINED: {
|
||||
if (analytics.rtcstatsEnabled && RTCStats.isInitialized()) {
|
||||
if (analytics.rtcstatsEndpoint && RTCStats.isInitialized()) {
|
||||
// Once the conference started connect to the rtcstats server and send data.
|
||||
try {
|
||||
RTCStats.connect();
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { VIDEO_QUALITY_LEVELS } from '../base/conference';
|
||||
|
||||
/**
|
||||
* Maps quality level names used in the config.videoQuality.minHeightForQualityLvl to the quality level constants used
|
||||
* by the application.
|
||||
* @type {Object}
|
||||
*/
|
||||
export const CFG_LVL_TO_APP_QUALITY_LVL = {
|
||||
'low': VIDEO_QUALITY_LEVELS.LOW,
|
||||
'standard': VIDEO_QUALITY_LEVELS.STANDARD,
|
||||
'high': VIDEO_QUALITY_LEVELS.HIGH
|
||||
};
|
||||
@@ -1,62 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import { VIDEO_QUALITY_LEVELS } from '../base/conference';
|
||||
|
||||
import { CFG_LVL_TO_APP_QUALITY_LVL } from './constants';
|
||||
|
||||
|
||||
/**
|
||||
* Selects {@code VIDEO_QUALITY_LEVELS} for the given {@link availableHeight} and threshold to quality mapping.
|
||||
*
|
||||
* @param {number} availableHeight - The height to which a matching video quality level should be found.
|
||||
* @param {Map<number, number>} heightToLevel - The threshold to quality level mapping. The keys are sorted in the
|
||||
* ascending order.
|
||||
* @returns {number} The matching value from {@code VIDEO_QUALITY_LEVELS}.
|
||||
*/
|
||||
export function getReceiverVideoQualityLevel(availableHeight: number, heightToLevel: Map<number, number>): number {
|
||||
let selectedLevel = VIDEO_QUALITY_LEVELS.LOW;
|
||||
|
||||
for (const [ levelThreshold, level ] of heightToLevel.entries()) {
|
||||
if (availableHeight >= levelThreshold) {
|
||||
selectedLevel = level;
|
||||
}
|
||||
}
|
||||
|
||||
return selectedLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts {@code Object} passed in the config which represents height thresholds to vide quality level mapping to
|
||||
* a {@code Map}.
|
||||
*
|
||||
* @param {Object} minHeightForQualityLvl - The 'config.videoQuality.minHeightForQualityLvl' Object from
|
||||
* the configuration. See config.js for more details.
|
||||
* @returns {Map<number, number>|undefined} - A mapping of minimal thumbnail height required for given quality level or
|
||||
* {@code undefined} if the map contains invalid values.
|
||||
*/
|
||||
export function validateMinHeightForQualityLvl(minHeightForQualityLvl: Object): ?Map<number, number> {
|
||||
if (typeof minHeightForQualityLvl !== 'object'
|
||||
|| Object.keys(minHeightForQualityLvl).map(lvl => Number(lvl))
|
||||
.find(lvl => lvl === null || isNaN(lvl) || lvl < 0)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const levelsSorted
|
||||
= Object.keys(minHeightForQualityLvl)
|
||||
.map(k => Number(k))
|
||||
.sort((a, b) => a - b);
|
||||
const map = new Map();
|
||||
|
||||
for (const level of levelsSorted) {
|
||||
const configQuality = minHeightForQualityLvl[level];
|
||||
const appQuality = CFG_LVL_TO_APP_QUALITY_LVL[configQuality];
|
||||
|
||||
if (!appQuality) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
map.set(level, appQuality);
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
@@ -1,4 +1,2 @@
|
||||
export * from './components';
|
||||
export * from './actions';
|
||||
|
||||
import './reducer';
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import {
|
||||
CONFERENCE_JOINED,
|
||||
VIDEO_QUALITY_LEVELS,
|
||||
getNearestReceiverVideoQualityLevel,
|
||||
setMaxReceiverVideoQuality,
|
||||
setPreferredVideoQuality
|
||||
} from '../base/conference';
|
||||
@@ -10,9 +11,7 @@ import { getParticipantCount } from '../base/participants';
|
||||
import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
|
||||
import { shouldDisplayTileView } from '../video-layout';
|
||||
|
||||
import { getReceiverVideoQualityLevel } from './functions';
|
||||
import logger from './logger';
|
||||
import { getMinHeightForQualityLvlMap } from './selector';
|
||||
|
||||
/**
|
||||
* Implements the middleware of the feature video-quality.
|
||||
@@ -67,7 +66,7 @@ StateListenerRegistry.register(
|
||||
if (reducedUI) {
|
||||
newMaxRecvVideoQuality = VIDEO_QUALITY_LEVELS.LOW;
|
||||
} else if (displayTileView && !Number.isNaN(thumbnailHeight)) {
|
||||
newMaxRecvVideoQuality = getReceiverVideoQualityLevel(thumbnailHeight, getMinHeightForQualityLvlMap(state));
|
||||
newMaxRecvVideoQuality = getNearestReceiverVideoQualityLevel(thumbnailHeight);
|
||||
|
||||
// Override HD level calculated for the thumbnail height when # of participants threshold is exceeded
|
||||
if (maxReceiverVideoQuality !== newMaxRecvVideoQuality && maxFullResolutionParticipants !== -1) {
|
||||
@@ -75,7 +74,7 @@ StateListenerRegistry.register(
|
||||
= participantCount > maxFullResolutionParticipants
|
||||
&& newMaxRecvVideoQuality > VIDEO_QUALITY_LEVELS.STANDARD;
|
||||
|
||||
logger.info(`Video quality level for thumbnail height: ${thumbnailHeight}, `
|
||||
logger.info(`The nearest receiver video quality level for thumbnail height: ${thumbnailHeight}, `
|
||||
+ `is: ${newMaxRecvVideoQuality}, `
|
||||
+ `override: ${String(override)}, `
|
||||
+ `max full res N: ${maxFullResolutionParticipants}`);
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import { VIDEO_QUALITY_LEVELS } from '../base/conference';
|
||||
import { SET_CONFIG } from '../base/config';
|
||||
import { ReducerRegistry, set } from '../base/redux';
|
||||
|
||||
import { validateMinHeightForQualityLvl } from './functions';
|
||||
import logger from './logger';
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
minHeightForQualityLvl: new Map()
|
||||
};
|
||||
|
||||
DEFAULT_STATE.minHeightForQualityLvl.set(360, VIDEO_QUALITY_LEVELS.STANDARD);
|
||||
DEFAULT_STATE.minHeightForQualityLvl.set(720, VIDEO_QUALITY_LEVELS.HIGH);
|
||||
|
||||
ReducerRegistry.register('features/base/videoquality', (state = DEFAULT_STATE, action) => {
|
||||
switch (action.type) {
|
||||
case SET_CONFIG:
|
||||
return _setConfig(state, action);
|
||||
}
|
||||
|
||||
return state;
|
||||
});
|
||||
|
||||
/**
|
||||
* Extracts the height to quality level mapping from the new config.
|
||||
*
|
||||
* @param {Object} state - The Redux state of feature base/lastn.
|
||||
* @param {Action} action - The Redux action SET_CONFIG to reduce.
|
||||
* @private
|
||||
* @returns {Object} The new state after the reduction of the specified action.
|
||||
*/
|
||||
function _setConfig(state, { config }) {
|
||||
const configuredMap = config?.videoQuality?.minHeightForQualityLvl;
|
||||
const convertedMap = validateMinHeightForQualityLvl(configuredMap);
|
||||
|
||||
if (configuredMap && !convertedMap) {
|
||||
logger.error('Invalid config value videoQuality.minHeightForQualityLvl');
|
||||
}
|
||||
|
||||
return convertedMap ? set(state, 'minHeightForQualityLvl', convertedMap) : state;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
// @flow
|
||||
|
||||
/**
|
||||
* Selects the thumbnail height to the quality level mapping from the config.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {Map<number,number>}
|
||||
*/
|
||||
export function getMinHeightForQualityLvlMap(state: Object): Map<number, number> {
|
||||
return state['features/base/videoquality'].minHeightForQualityLvl;
|
||||
}
|
||||
@@ -77,18 +77,9 @@ end
|
||||
function provider.get_sasl_handler(session)
|
||||
|
||||
local function get_username_from_token(self, message)
|
||||
|
||||
-- retrieve custom public key from server and save it on the session
|
||||
local pre_event_result = prosody.events.fire_event("pre-jitsi-authentication-fetch-key", session);
|
||||
if pre_event_result ~= nil and pre_event_result.res == false then
|
||||
log("warn",
|
||||
"Error verifying token on pre authentication stage:%s, reason:%s", pre_event_result.error, pre_event_result.reason);
|
||||
session.auth_token = nil;
|
||||
return pre_event_result.res, pre_event_result.error, pre_event_result.reason;
|
||||
end
|
||||
|
||||
local res, error, reason = token_util:process_and_verify_token(session);
|
||||
if res == false then
|
||||
|
||||
if (res == false) then
|
||||
log("warn",
|
||||
"Error verifying token err:%s, reason:%s", error, reason);
|
||||
session.auth_token = nil;
|
||||
@@ -111,14 +102,6 @@ function provider.get_sasl_handler(session)
|
||||
self.username = message;
|
||||
end
|
||||
|
||||
local post_event_result = prosody.events.fire_event("post-jitsi-authentication", session);
|
||||
if post_event_result ~= nil and post_event_result.res == false then
|
||||
log("warn",
|
||||
"Error verifying token on post authentication stage :%s, reason:%s", post_event_result.error, post_event_result.reason);
|
||||
session.auth_token = nil;
|
||||
return post_event_result.res, post_event_result.error, post_event_result.reason;
|
||||
end
|
||||
|
||||
return res;
|
||||
end
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
local jibri_queue_component
|
||||
= module:get_option_string(
|
||||
"jibri_queue_component", "jibriqueue"..module.host);
|
||||
|
||||
module:add_identity("component", "jibri-queue", jibri_queue_component);
|
||||
@@ -1,559 +0,0 @@
|
||||
local st = require "util.stanza";
|
||||
local jid = require "util.jid";
|
||||
local http = require "net.http";
|
||||
local json = require "cjson";
|
||||
local inspect = require('inspect');
|
||||
local socket = require "socket";
|
||||
local uuid_gen = require "util.uuid".generate;
|
||||
local jwt = require "luajwtjitsi";
|
||||
local it = require "util.iterators";
|
||||
local neturl = require "net.url";
|
||||
local parse = neturl.parseQuery;
|
||||
|
||||
local get_room_from_jid = module:require "util".get_room_from_jid;
|
||||
local room_jid_match_rewrite = module:require "util".room_jid_match_rewrite;
|
||||
local is_healthcheck_room = module:require "util".is_healthcheck_room;
|
||||
local room_jid_split_subdomain = module:require "util".room_jid_split_subdomain;
|
||||
local internal_room_jid_match_rewrite = module:require "util".internal_room_jid_match_rewrite;
|
||||
local async_handler_wrapper = module:require "util".async_handler_wrapper;
|
||||
|
||||
-- this basically strips the domain from the conference.domain address
|
||||
local parentHostName = string.gmatch(tostring(module.host), "%w+.(%w.+)")();
|
||||
if parentHostName == nil then
|
||||
log("error", "Failed to start - unable to get parent hostname");
|
||||
return;
|
||||
end
|
||||
|
||||
local parentCtx = module:context(parentHostName);
|
||||
if parentCtx == nil then
|
||||
log("error",
|
||||
"Failed to start - unable to get parent context for host: %s",
|
||||
tostring(parentHostName));
|
||||
return;
|
||||
end
|
||||
local token_util = module:require "token/util".new(parentCtx);
|
||||
|
||||
local ASAPKeyServer;
|
||||
local ASAPKeyPath;
|
||||
local ASAPKeyId;
|
||||
local ASAPIssuer;
|
||||
local ASAPAudience;
|
||||
local ASAPAcceptedIssuers;
|
||||
local ASAPAcceptedAudiences;
|
||||
local ASAPTTL;
|
||||
local ASAPTTL_THRESHOLD;
|
||||
local ASAPKey;
|
||||
local JibriRegion;
|
||||
local disableTokenVerification;
|
||||
local muc_component_host;
|
||||
local external_api_url;
|
||||
local jwtKeyCacheSize;
|
||||
local jwtKeyCache;
|
||||
|
||||
local function load_config()
|
||||
ASAPKeyServer = module:get_option_string("asap_key_server");
|
||||
|
||||
if ASAPKeyServer then
|
||||
module:log("debug", "ASAP Public Key URL %s", ASAPKeyServer);
|
||||
token_util:set_asap_key_server(ASAPKeyServer);
|
||||
end
|
||||
|
||||
ASAPKeyPath
|
||||
= module:get_option_string("asap_key_path", '/etc/prosody/certs/asap.key');
|
||||
|
||||
ASAPKeyId
|
||||
= module:get_option_string("asap_key_id", 'jitsi');
|
||||
|
||||
ASAPIssuer
|
||||
= module:get_option_string("asap_issuer", 'jitsi');
|
||||
|
||||
ASAPAudience
|
||||
= module:get_option_string("asap_audience", 'jibri-queue');
|
||||
|
||||
ASAPAcceptedIssuers
|
||||
= module:get_option_array('asap_accepted_issuers',{'jibri-queue'});
|
||||
module:log("debug", "ASAP Accepted Issuers %s", ASAPAcceptedIssuers);
|
||||
token_util:set_asap_accepted_issuers(ASAPAcceptedIssuers);
|
||||
|
||||
ASAPAcceptedAudiences
|
||||
= module:get_option_array('asap_accepted_audiences',{'*'});
|
||||
module:log("debug", "ASAP Accepted Audiences %s", ASAPAcceptedAudiences);
|
||||
token_util:set_asap_accepted_audiences(ASAPAcceptedAudiences);
|
||||
|
||||
-- do not require room to be set on tokens for jibri queue
|
||||
token_util:set_asap_require_room_claim(false);
|
||||
|
||||
ASAPTTL
|
||||
= module:get_option_number("asap_ttl", 3600);
|
||||
|
||||
ASAPTTL_THRESHOLD
|
||||
= module:get_option_number("asap_ttl_threshold", 600);
|
||||
|
||||
queueServiceURL
|
||||
= module:get_option_string("jibri_queue_url");
|
||||
|
||||
JibriRegion
|
||||
= module:get_option_string("jibri_region", 'default');
|
||||
|
||||
-- option to enable/disable token verifications
|
||||
disableTokenVerification
|
||||
= module:get_option_boolean("disable_jibri_queue_token_verification", false);
|
||||
|
||||
muc_component_host
|
||||
= module:get_option_string("muc_component");
|
||||
|
||||
external_api_url = module:get_option_string("external_api_url",tostring(parentHostName));
|
||||
module:log("debug", "External advertised API URL", external_api_url);
|
||||
|
||||
|
||||
-- TODO: Figure out a less arbitrary default cache size.
|
||||
jwtKeyCacheSize
|
||||
= module:get_option_number("jwt_pubkey_cache_size", 128);
|
||||
jwtKeyCache = require"util.cache".new(jwtKeyCacheSize);
|
||||
|
||||
if queueServiceURL == nil then
|
||||
log("error", "No jibri_queue_url specified. No service to contact!");
|
||||
return;
|
||||
end
|
||||
|
||||
if muc_component_host == nil then
|
||||
log("error", "No muc_component specified. No muc to operate on for jibri queue!");
|
||||
return;
|
||||
end
|
||||
|
||||
-- Read ASAP key once on module startup
|
||||
local f = io.open(ASAPKeyPath, "r");
|
||||
if f then
|
||||
ASAPKey = f:read("*all");
|
||||
f:close();
|
||||
if not ASAPKey then
|
||||
module:log("warn", "No ASAP Key read from %s, disabling jibri queue component plugin", ASAPKeyPath);
|
||||
return
|
||||
end
|
||||
else
|
||||
module:log("warn", "Error reading ASAP Key %s, disabling jibri queue component plugin", ASAPKeyPath);
|
||||
return
|
||||
end
|
||||
|
||||
return true;
|
||||
end
|
||||
|
||||
local function reload_config()
|
||||
module:log("info", "Reloading configuration for jibri queue component");
|
||||
local config_success = load_config();
|
||||
|
||||
-- clear ASAP public key cache on config reload
|
||||
token_util:clear_asap_cache();
|
||||
|
||||
if not config_success then
|
||||
log("error", "Unsuccessful reconfiguration, jibri queue component may misbehave");
|
||||
end
|
||||
end
|
||||
|
||||
local config_success = load_config();
|
||||
|
||||
if not config_success then
|
||||
log("error", "Unsuccessful configuration step, jibri queue component disabled")
|
||||
return;
|
||||
end
|
||||
|
||||
|
||||
local http_headers = {
|
||||
["User-Agent"] = "Prosody ("..prosody.version.."; "..prosody.platform..")",
|
||||
["Content-Type"] = "application/json"
|
||||
};
|
||||
|
||||
-- we use async to detect Prosody 0.10 and earlier
|
||||
local have_async = pcall(require, "util.async");
|
||||
if not have_async then
|
||||
module:log("warn", "conference duration will not work with Prosody version 0.10 or less.");
|
||||
return;
|
||||
end
|
||||
|
||||
|
||||
log("info", "Starting jibri queue handling for %s", muc_component_host);
|
||||
|
||||
local function round(num, numDecimalPlaces)
|
||||
local mult = 10^(numDecimalPlaces or 0)
|
||||
return math.floor(num * mult + 0.5) / mult
|
||||
end
|
||||
|
||||
local function generateToken(audience)
|
||||
audience = audience or ASAPAudience
|
||||
local t = os.time()
|
||||
local err
|
||||
local exp_key = 'asap_exp.'..audience
|
||||
local token_key = 'asap_token.'..audience
|
||||
local exp = jwtKeyCache:get(exp_key)
|
||||
local token = jwtKeyCache:get(token_key)
|
||||
|
||||
--if we find a token and it isn't too far from expiry, then use it
|
||||
if token ~= nil and exp ~= nil then
|
||||
exp = tonumber(exp)
|
||||
if (exp - t) > ASAPTTL_THRESHOLD then
|
||||
return token
|
||||
end
|
||||
end
|
||||
|
||||
--expiry is the current time plus TTL
|
||||
exp = t + ASAPTTL
|
||||
local payload = {
|
||||
iss = ASAPIssuer,
|
||||
aud = audience,
|
||||
nbf = t,
|
||||
exp = exp,
|
||||
}
|
||||
|
||||
-- encode
|
||||
local alg = "RS256"
|
||||
token, err = jwt.encode(payload, ASAPKey, alg, {kid = ASAPKeyId})
|
||||
if not err then
|
||||
token = 'Bearer '..token
|
||||
jwtKeyCache:set(exp_key,exp)
|
||||
jwtKeyCache:set(token_key,token)
|
||||
return token
|
||||
else
|
||||
return ''
|
||||
end
|
||||
end
|
||||
|
||||
local function sendIq(participant,action,requestId,time,position,token)
|
||||
local iqId = uuid_gen();
|
||||
local from = module:get_host();
|
||||
local outStanza = st.iq({type = 'set', from = from, to = participant, id = iqId}):tag("jibri-queue",
|
||||
{ xmlns = 'http://jitsi.org/protocol/jibri-queue', requestId = requestId, action = action });
|
||||
|
||||
if token then
|
||||
outStanza:tag("token"):text(token):up()
|
||||
end
|
||||
if time then
|
||||
outStanza:tag("time"):text(tostring(time)):up()
|
||||
end
|
||||
if position then
|
||||
outStanza:tag("position"):text(tostring(position)):up()
|
||||
end
|
||||
|
||||
module:send(outStanza);
|
||||
end
|
||||
|
||||
local function cb(content_, code_, response_, request_)
|
||||
if code_ == 200 or code_ == 204 then
|
||||
module:log("debug", "URL Callback: Code %s, Content %s, Request (host %s, path %s, body %s), Response: %s",
|
||||
code_, content_, request_.host, request_.path, inspect(request_.body), inspect(response_));
|
||||
else
|
||||
module:log("warn", "URL Callback non successful: Code %s, Content %s, Request (%s), Response: %s",
|
||||
code_, content_, inspect(request_), inspect(response_));
|
||||
end
|
||||
end
|
||||
|
||||
local function sendEvent(type,room_address,participant,requestId,replyIq,replyError)
|
||||
local event_ts = round(socket.gettime()*1000);
|
||||
local node, host, resource, target_subdomain = room_jid_split_subdomain(room_address);
|
||||
local room_param = '';
|
||||
if target_subdomain then
|
||||
room_param = target_subdomain..'/'..node;
|
||||
else
|
||||
room_param = node;
|
||||
end
|
||||
|
||||
local out_event = {
|
||||
["conference"] = room_address,
|
||||
["roomParam"] = room_param,
|
||||
["eventType"] = type,
|
||||
["participant"] = participant,
|
||||
["externalApiUrl"] = external_api_url.."/jibriqueue/update",
|
||||
["requestId"] = requestId,
|
||||
["region"] = JibriRegion,
|
||||
}
|
||||
module:log("debug","Sending event %s",inspect(out_event));
|
||||
|
||||
local headers = http_headers or {}
|
||||
headers['Authorization'] = generateToken()
|
||||
|
||||
module:log("debug","Sending headers %s",inspect(headers));
|
||||
local requestURL = queueServiceURL.."/job/recording"
|
||||
if type=="LeaveQueue" then
|
||||
requestURL = requestURL .."/cancel"
|
||||
end
|
||||
local request = http.request(requestURL, {
|
||||
headers = headers,
|
||||
method = "POST",
|
||||
body = json.encode(out_event)
|
||||
}, function (content_, code_, response_, request_)
|
||||
if code_ == 200 or code_ == 204 then
|
||||
module:log("debug", "URL Callback: Code %s, Content %s, Request (host %s, path %s, body %s), Response: %s",
|
||||
code_, content_, request_.host, request_.path, inspect(request_.body), inspect(response_));
|
||||
if (replyIq) then
|
||||
module:log("debug", "sending reply IQ %s",inspect(replyIq));
|
||||
module:send(replyIq);
|
||||
end
|
||||
else
|
||||
module:log("warn", "URL Callback non successful: Code %s, Content %s, Request (%s), Response: %s",
|
||||
code_, content_, inspect(request_), inspect(response_));
|
||||
if (replyError) then
|
||||
module:log("warn", "sending reply error IQ %s",inspect(replyError));
|
||||
module:send(replyError);
|
||||
end
|
||||
end
|
||||
end);
|
||||
end
|
||||
|
||||
function clearRoomQueueByOccupant(room, occupant)
|
||||
room.jibriQueue[occupant.jid] = nil;
|
||||
end
|
||||
|
||||
function addRoomQueueByOccupant(room, occupant, requestId)
|
||||
room.jibriQueue[occupant.jid] = requestId;
|
||||
end
|
||||
|
||||
-- receives iq from client currently connected to the room
|
||||
function on_iq(event)
|
||||
local requestId;
|
||||
-- Check the type of the incoming stanza to avoid loops:
|
||||
if event.stanza.attr.type == "error" then
|
||||
return; -- We do not want to reply to these, so leave.
|
||||
end
|
||||
if event.stanza.attr.to == module:get_host() then
|
||||
if event.stanza.attr.type == "set" then
|
||||
local reply = st.reply(event.stanza);
|
||||
local replyError = st.error_reply(event.stanza,'cancel','internal-server-error',"Queue Server Error");
|
||||
|
||||
local jibriQueue
|
||||
= event.stanza:get_child('jibri-queue', 'http://jitsi.org/protocol/jibri-queue');
|
||||
if jibriQueue then
|
||||
module:log("debug", "Received Jibri Queue Request: %s ",inspect(jibriQueue));
|
||||
|
||||
local roomAddress = jibriQueue.attr.room;
|
||||
local room = get_room_from_jid(room_jid_match_rewrite(roomAddress));
|
||||
|
||||
if not room then
|
||||
module:log("warn", "No room found %s", roomAddress);
|
||||
return false;
|
||||
end
|
||||
|
||||
local from = event.stanza.attr.from;
|
||||
|
||||
local occupant = room:get_occupant_by_real_jid(from);
|
||||
if not occupant then
|
||||
module:log("warn", "No occupant %s found for %s", from, roomAddress);
|
||||
return false;
|
||||
end
|
||||
|
||||
local action = jibriQueue.attr.action;
|
||||
if action == 'join' then
|
||||
-- join action, so send event out
|
||||
requestId = uuid_gen();
|
||||
module:log("debug","Received join queue request for jid %s occupant %s requestId %s",roomAddress,occupant.jid,requestId);
|
||||
|
||||
-- now handle new jibri queue message
|
||||
addRoomQueueByOccupant(room, occupant, requestId);
|
||||
reply:add_child(st.stanza("jibri-queue", { xmlns = 'http://jitsi.org/protocol/jibri-queue', requestId = requestId})):up()
|
||||
replyError:add_child(st.stanza("jibri-queue", { xmlns = 'http://jitsi.org/protocol/jibri-queue', requestId = requestId})):up()
|
||||
|
||||
module:log("debug","Sending JoinQueue event for jid %s occupant %s reply %s",roomAddress,occupant.jid,inspect(reply));
|
||||
sendEvent('JoinQueue',roomAddress,occupant.jid,requestId,reply,replyError);
|
||||
end
|
||||
if action == 'leave' then
|
||||
requestId = jibriQueue.attr.requestId;
|
||||
module:log("debug","Received leave queue request for jid %s occupant %s requestId %s",roomAddress,occupant.jid,requestId);
|
||||
|
||||
-- TODO: check that requestId is the same as cached value
|
||||
clearRoomQueueByOccupant(room, occupant);
|
||||
reply:add_child(st.stanza("jibri-queue", { xmlns = 'http://jitsi.org/protocol/jibri-queue', requestId = requestId})):up()
|
||||
replyError:add_child(st.stanza("jibri-queue", { xmlns = 'http://jitsi.org/protocol/jibri-queue', requestId = requestId})):up()
|
||||
|
||||
module:log("debug","Sending LeaveQueue event for jid %s occupant %s reply %s",roomAddress,occupant.jid,inspect(reply));
|
||||
sendEvent('LeaveQueue',roomAddress,occupant.jid,requestId,reply,replyError);
|
||||
end
|
||||
else
|
||||
module:log("warn","Jibri Queue Stanza missing child %s",inspect(event.stanza))
|
||||
end
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
-- create recorder queue cache for the room
|
||||
function room_created(event)
|
||||
local room = event.room;
|
||||
|
||||
if is_healthcheck_room(room.jid) then
|
||||
return;
|
||||
end
|
||||
|
||||
room.jibriQueue = {};
|
||||
end
|
||||
|
||||
-- Conference ended, clear all queue cache jids
|
||||
function room_destroyed(event)
|
||||
local room = event.room;
|
||||
|
||||
if is_healthcheck_room(room.jid) then
|
||||
return;
|
||||
end
|
||||
for jid, x in pairs(room.jibriQueue) do
|
||||
if x then
|
||||
sendEvent('LeaveQueue',internal_room_jid_match_rewrite(room.jid),jid,x);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Occupant left remove it from the queue if it joined the queue
|
||||
function occupant_leaving(event)
|
||||
local room = event.room;
|
||||
|
||||
if is_healthcheck_room(room.jid) then
|
||||
return;
|
||||
end
|
||||
|
||||
local occupant = event.occupant;
|
||||
local requestId = room.jibriQueue[occupant.jid];
|
||||
-- check if user has cached queue request
|
||||
if requestId then
|
||||
-- remove occupant from queue cache, signal backend
|
||||
room.jibriQueue[occupant.jid] = nil;
|
||||
sendEvent('LeaveQueue',internal_room_jid_match_rewrite(room.jid),occupant.jid,requestId);
|
||||
end
|
||||
end
|
||||
|
||||
module:hook("iq/host", on_iq);
|
||||
|
||||
-- executed on every host added internally in prosody, including components
|
||||
function process_host(host)
|
||||
if host == muc_component_host then -- the conference muc component
|
||||
module:log("debug","Hook to muc events on %s", host);
|
||||
|
||||
local muc_module = module:context(host);
|
||||
muc_module:hook("muc-room-created", room_created, -1);
|
||||
-- muc_module:hook("muc-occupant-joined", occupant_joined, -1);
|
||||
muc_module:hook("muc-occupant-pre-leave", occupant_leaving, -1);
|
||||
muc_module:hook("muc-room-destroyed", room_destroyed, -1);
|
||||
end
|
||||
end
|
||||
|
||||
if prosody.hosts[muc_component_host] == nil then
|
||||
module:log("debug","No muc component found, will listen for it: %s", muc_component_host)
|
||||
|
||||
-- when a host or component is added
|
||||
prosody.events.add_handler("host-activated", process_host);
|
||||
else
|
||||
process_host(muc_component_host);
|
||||
end
|
||||
|
||||
module:log("info", "Loading jibri_queue_component");
|
||||
|
||||
--- Verifies room name, domain name with the values in the token
|
||||
-- @param token the token we received
|
||||
-- @param room_name the room name
|
||||
-- @param group name of the group (optional)
|
||||
-- @param session the session to use for storing token specific fields
|
||||
-- @return true if values are ok or false otherwise
|
||||
function verify_token(token, room_jid, session)
|
||||
if disableTokenVerification then
|
||||
return true;
|
||||
end
|
||||
|
||||
-- if not disableTokenVerification and we do not have token
|
||||
-- stop here, cause the main virtual host can have guest access enabled
|
||||
-- (allowEmptyToken = true) and we will allow access to rooms info without
|
||||
-- a token
|
||||
if token == nil then
|
||||
log("warn", "no token provided");
|
||||
return false;
|
||||
end
|
||||
|
||||
session.auth_token = token;
|
||||
local verified, reason, message = token_util:process_and_verify_token(session);
|
||||
if not verified then
|
||||
log("warn", "not a valid token %s: %s", tostring(reason), tostring(message));
|
||||
log("debug", "invalid token %s", token);
|
||||
return false;
|
||||
end
|
||||
|
||||
return true;
|
||||
end
|
||||
|
||||
--- Handles request for updating jibri queue status
|
||||
-- @param event the http event, holds the request query
|
||||
-- @return GET response, containing a json with response details
|
||||
function handle_update_jibri_queue(event)
|
||||
local body = json.decode(event.request.body);
|
||||
|
||||
module:log("debug","Update Jibri Queue Event Received: %s",inspect(body));
|
||||
|
||||
local token = event.request.headers["authorization"];
|
||||
if not token then
|
||||
token = ''
|
||||
else
|
||||
local prefixStart, prefixEnd = token:find("Bearer ");
|
||||
if prefixStart ~= 1 then
|
||||
module:log("error", "REST event: Invalid authorization header format. The header must start with the string 'Bearer '");
|
||||
return { status_code = 403; };
|
||||
end
|
||||
token = token:sub(prefixEnd + 1);
|
||||
end
|
||||
|
||||
local user_jid = body["participant"];
|
||||
local roomAddress = body["conference"];
|
||||
local userJWT = body["token"];
|
||||
local action = body["action"];
|
||||
local time = body["time"];
|
||||
local position = body["position"];
|
||||
local requestId = body["requestId"];
|
||||
|
||||
if not action then
|
||||
if userJWT then
|
||||
action = 'token';
|
||||
else
|
||||
action = 'info';
|
||||
end
|
||||
end
|
||||
|
||||
local room_jid = room_jid_match_rewrite(roomAddress);
|
||||
|
||||
if not verify_token(token, room_jid, {}) then
|
||||
log("error", "REST event: Invalid token for room %s to route action %s for requestId %s", roomAddress, action, requestId);
|
||||
return { status_code = 403; };
|
||||
end
|
||||
|
||||
local room = get_room_from_jid(room_jid);
|
||||
if (not room) then
|
||||
log("error", "REST event: no room found %s to route action %s for requestId %s", roomAddress, action, requestId);
|
||||
return { status_code = 404; };
|
||||
end
|
||||
|
||||
local occupant = room:get_occupant_by_real_jid(user_jid);
|
||||
if not occupant then
|
||||
log("warn", "REST event: No occupant %s found for %s to route action %s for requestId %s", user_jid, roomAddress, action, requestId);
|
||||
return { status_code = 404; };
|
||||
end
|
||||
|
||||
if not room.jibriQueue[occupant.jid] then
|
||||
log("warn", "REST event: No queue request found for occupant %s in conference %s to route action %s for requestId %s",occupant.jid,room.jid, action, requestId)
|
||||
return { status_code = 404; };
|
||||
end
|
||||
|
||||
if not requestId then
|
||||
requestId = room.jibriQueue[occupant.jid];
|
||||
end
|
||||
|
||||
if action == 'token' and userJWT then
|
||||
log("debug", "REST event: Token received for occupant %s in conference %s requestId %s, clearing room queue");
|
||||
clearRoomQueueByOccupant(room, occupant);
|
||||
end
|
||||
|
||||
log("debug", "REST event: Sending update for occupant %s in conference %s to route action %s for requestId %s",occupant.jid,room.jid, action, requestId);
|
||||
sendIq(occupant.jid,action,requestId,time,position,userJWT);
|
||||
return { status_code = 200; };
|
||||
end
|
||||
|
||||
module:depends("http");
|
||||
module:provides("http", {
|
||||
default_path = "/";
|
||||
name = "jibriqueue";
|
||||
route = {
|
||||
["POST /jibriqueue/update"] = function (event) return async_handler_wrapper(event,handle_update_jibri_queue) end;
|
||||
};
|
||||
});
|
||||
|
||||
module:hook_global('config-reloaded', reload_config);
|
||||
@@ -132,7 +132,7 @@ function filter_stanza(stanza)
|
||||
|
||||
-- check is an owner, only owners can receive the presence
|
||||
local room = main_muc_service.get_room_from_jid(jid_bare(node .. '@' .. main_muc_component_config));
|
||||
if not room or room.get_affiliation(room, stanza.attr.to) == 'owner' then
|
||||
if room.get_affiliation(room, stanza.attr.to) == 'owner' then
|
||||
return stanza;
|
||||
end
|
||||
|
||||
@@ -159,11 +159,6 @@ function attach_lobby_room(room)
|
||||
local lobby_room_jid = node .. '@' .. lobby_muc_component_config;
|
||||
if not lobby_muc_service.get_room_from_jid(lobby_room_jid) then
|
||||
local new_room = lobby_muc_service.create_room(lobby_room_jid);
|
||||
-- set persistent the lobby room to avoid it to be destroyed
|
||||
-- there are cases like when selecting new moderator after the current one leaves
|
||||
-- which can leave the room with no occupants and it will be destroyed and we want to
|
||||
-- avoid lobby destroy while it is enabled
|
||||
new_room:set_persistent(true);
|
||||
module:log("debug","Lobby room jid = %s created",lobby_room_jid);
|
||||
new_room.main_room = room;
|
||||
room._data.lobbyroom = new_room;
|
||||
@@ -173,18 +168,6 @@ function attach_lobby_room(room)
|
||||
return false
|
||||
end
|
||||
|
||||
-- destroys lobby room for the supplied main room
|
||||
function destroy_lobby_room(room, newjid, message)
|
||||
if not message then
|
||||
message = 'Lobby room closed.';
|
||||
end
|
||||
if room and room._data.lobbyroom then
|
||||
room._data.lobbyroom:set_persistent(false);
|
||||
room._data.lobbyroom:destroy(newjid, message);
|
||||
room._data.lobbyroom = nil;
|
||||
end
|
||||
end
|
||||
|
||||
-- process a host module directly if loaded or hooks to wait for its load
|
||||
function process_host_module(name, callback)
|
||||
local function process_host(host)
|
||||
@@ -297,14 +280,16 @@ process_host_module(main_muc_component_config, function(host_module, host)
|
||||
notify_lobby_enabled(room, actor, true);
|
||||
end
|
||||
elseif room._data.lobbyroom then
|
||||
destroy_lobby_room(room, room.jid);
|
||||
room._data.lobbyroom:destroy(room.jid, 'Lobby room closed.');
|
||||
room._data.lobbyroom = nil;
|
||||
notify_lobby_enabled(room, actor, false);
|
||||
end
|
||||
end);
|
||||
host_module:hook('muc-room-destroyed',function(event)
|
||||
local room = event.room;
|
||||
if room._data.lobbyroom then
|
||||
destroy_lobby_room(room, nil);
|
||||
room._data.lobbyroom:destroy(nil, 'Lobby room closed.');
|
||||
room._data.lobbyroom = nil;
|
||||
end
|
||||
end);
|
||||
host_module:hook('muc-disco#info', function (event)
|
||||
@@ -414,12 +399,7 @@ function handle_create_lobby(event)
|
||||
attach_lobby_room(room)
|
||||
end
|
||||
|
||||
function handle_destroy_lobby(event)
|
||||
destroy_lobby_room(event.room, event.newjid, event.message);
|
||||
end
|
||||
|
||||
module:hook_global('bosh-session', update_session);
|
||||
module:hook_global('websocket-session', update_session);
|
||||
module:hook_global('config-reloaded', load_config);
|
||||
module:hook_global('create-lobby-room', handle_create_lobby);
|
||||
module:hook_global('destroy-lobby-room', handle_destroy_lobby);
|
||||
|
||||
@@ -5,13 +5,17 @@ local basexx = require "basexx";
|
||||
local have_async, async = pcall(require, "util.async");
|
||||
local hex = require "util.hex";
|
||||
local jwt = require "luajwtjitsi";
|
||||
local http = require "net.http";
|
||||
local jid = require "util.jid";
|
||||
local json_safe = require "cjson.safe";
|
||||
local path = require "util.paths";
|
||||
local sha256 = require "util.hashes".sha256;
|
||||
local http_get_with_retry = module:require "util".http_get_with_retry;
|
||||
local timer = require "util.timer";
|
||||
|
||||
local nr_retries = 3;
|
||||
local http_timeout = 30;
|
||||
local http_headers = {
|
||||
["User-Agent"] = "Prosody ("..prosody.version.."; "..prosody.platform..")"
|
||||
};
|
||||
|
||||
-- TODO: Figure out a less arbitrary default cache size.
|
||||
local cacheSize = module:get_option_number("jwt_pubkey_cache_size", 128);
|
||||
@@ -89,8 +93,6 @@ function Util.new(module)
|
||||
--array of accepted audiences: by default only includes our appId
|
||||
self.acceptedAudiences = module:get_option_array('asap_accepted_audiences',{'*'})
|
||||
|
||||
self.requireRoomClaim = module:get_option_boolean('asap_require_room_claim', true);
|
||||
|
||||
if self.asapKeyServer and not have_async then
|
||||
module:log("error", "requires a version of Prosody with util.async");
|
||||
return nil;
|
||||
@@ -100,23 +102,7 @@ function Util.new(module)
|
||||
end
|
||||
|
||||
function Util:set_asap_key_server(asapKeyServer)
|
||||
self.asapKeyServer = asapKeyServer;
|
||||
end
|
||||
|
||||
function Util:set_asap_accepted_issuers(acceptedIssuers)
|
||||
self.acceptedIssuers = acceptedIssuers;
|
||||
end
|
||||
|
||||
function Util:set_asap_accepted_audiences(acceptedAudiences)
|
||||
self.acceptedAudiences = acceptedAudiences;
|
||||
end
|
||||
|
||||
function Util:set_asap_require_room_claim(checkRoom)
|
||||
self.requireRoomClaim = checkRoom;
|
||||
end
|
||||
|
||||
function Util:clear_asap_cache()
|
||||
self.cache = require"util.cache".new(cacheSize);
|
||||
self.asapKeyServer = asapKeyServer
|
||||
end
|
||||
|
||||
--- Returns the public key by keyID
|
||||
@@ -127,18 +113,55 @@ function Util:get_public_key(keyId)
|
||||
if content == nil then
|
||||
-- If the key is not found in the cache.
|
||||
module:log("debug", "Cache miss for key: "..keyId);
|
||||
local code;
|
||||
local wait, done = async.waiter();
|
||||
local function cb(content_, code_, response_, request_)
|
||||
content, code = content_, code_;
|
||||
if code == 200 or code == 204 then
|
||||
self.cache:set(keyId, content);
|
||||
else
|
||||
module:log("warn", "Error on public key request: Code %s, Content %s",
|
||||
code_, content_);
|
||||
end
|
||||
done();
|
||||
end
|
||||
local keyurl = path.join(self.asapKeyServer, hex.to(sha256(keyId))..'.pem');
|
||||
module:log("debug", "Fetching public key from: "..keyurl);
|
||||
content = http_get_with_retry(keyurl, nr_retries);
|
||||
if content ~= nil then
|
||||
self.cache:set(keyId, content);
|
||||
|
||||
-- We hash the key ID to work around some legacy behavior and make
|
||||
-- deployment easier. It also helps prevent directory
|
||||
-- traversal attacks (although path cleaning could have done this too).
|
||||
local request = http.request(keyurl, {
|
||||
headers = http_headers or {},
|
||||
method = "GET"
|
||||
}, cb);
|
||||
|
||||
-- TODO: Is the done() call racey? Can we cancel this if the request
|
||||
-- succeedes?
|
||||
local function cancel()
|
||||
-- TODO: This check is racey. Not likely to be a problem, but we should
|
||||
-- still stick a mutex on content / code at some point.
|
||||
if code == nil then
|
||||
-- no longer present in prosody 0.11, so check before calling
|
||||
if http.destroy_request ~= nil then
|
||||
http.destroy_request(request);
|
||||
end
|
||||
done();
|
||||
end
|
||||
end
|
||||
timer.add_task(http_timeout, cancel);
|
||||
wait();
|
||||
|
||||
if code == 200 or code == 204 then
|
||||
return content;
|
||||
end
|
||||
return content;
|
||||
else
|
||||
-- If the key is in the cache, use it.
|
||||
module:log("debug", "Cache hit for key: "..keyId);
|
||||
return content;
|
||||
end
|
||||
|
||||
return nil;
|
||||
end
|
||||
|
||||
--- Verifies issuer part of token
|
||||
@@ -146,10 +169,6 @@ end
|
||||
-- @param 'acceptedIssuers' list of issuers to check
|
||||
-- @return nil and error string or true for accepted claim
|
||||
function Util:verify_issuer(issClaim, acceptedIssuers)
|
||||
if not acceptedIssuers then
|
||||
acceptedIssuers = self.acceptedIssuers
|
||||
end
|
||||
module:log("debug","verify_issuer claim: %s against accepted: %s",issClaim, acceptedIssuers);
|
||||
for i, iss in ipairs(acceptedIssuers) do
|
||||
if issClaim == iss then
|
||||
--claim matches an accepted issuer so return success
|
||||
@@ -164,7 +183,6 @@ end
|
||||
-- @param 'aud' claim from the token to verify
|
||||
-- @return nil and error string or true for accepted claim
|
||||
function Util:verify_audience(audClaim)
|
||||
module:log("debug","verify_audience claim: %s against accepted: %s",audClaim, self.acceptedAudiences);
|
||||
for i, aud in ipairs(self.acceptedAudiences) do
|
||||
if aud == '*' then
|
||||
--* indicates to accept any audience in the claims so return success
|
||||
@@ -205,11 +223,9 @@ function Util:verify_token(token, secret, acceptedIssuers)
|
||||
return nil, issCheckErr;
|
||||
end
|
||||
|
||||
if self.requireRoomClaim then
|
||||
local roomClaim = claims["room"];
|
||||
if roomClaim == nil then
|
||||
return nil, "'room' claim is missing";
|
||||
end
|
||||
local roomClaim = claims["room"];
|
||||
if roomClaim == nil then
|
||||
return nil, "'room' claim is missing";
|
||||
end
|
||||
|
||||
local audClaim = claims["aud"];
|
||||
@@ -250,10 +266,7 @@ function Util:process_and_verify_token(session, acceptedIssuers)
|
||||
end
|
||||
|
||||
local pubKey;
|
||||
if session.public_key then
|
||||
module:log("debug","Public key was found on the session");
|
||||
pubKey = session.public_key;
|
||||
elseif self.asapKeyServer and session.auth_token ~= nil then
|
||||
if self.asapKeyServer and session.auth_token ~= nil then
|
||||
local dotFirst = session.auth_token:find("%.");
|
||||
if not dotFirst then return nil, "Invalid token" end
|
||||
local header, err = json_safe.decode(basexx.from_url64(session.auth_token:sub(1,dotFirst-1)));
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user