mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-05-08 20:12:30 +00:00
Compare commits
27 Commits
android-sd
...
7630
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11f0ab9226 | ||
|
|
0b6705610c | ||
|
|
9d9199ba3b | ||
|
|
ff656a0625 | ||
|
|
148fc103e3 | ||
|
|
77abbee308 | ||
|
|
83c4ce98b4 | ||
|
|
898741e40d | ||
|
|
0c3e7395e7 | ||
|
|
c530bdd107 | ||
|
|
29dbcb309d | ||
|
|
8a4990d9ae | ||
|
|
0e55cbbda6 | ||
|
|
6da94aecf2 | ||
|
|
2a3c962e88 | ||
|
|
34f1eb60f4 | ||
|
|
4115ebe856 | ||
|
|
d7dadfc157 | ||
|
|
2851eeeab3 | ||
|
|
84d75f2ae8 | ||
|
|
73b3309adf | ||
|
|
e2de06f60d | ||
|
|
cdc7962d11 | ||
|
|
59242e1217 | ||
|
|
631e39d4fd | ||
|
|
4290cdf53d | ||
|
|
84c1e20216 |
2
Makefile
2
Makefile
@@ -55,6 +55,8 @@ deploy-appbundle:
|
||||
$(BUILD_DIR)/face-landmarks-worker.min.js.map \
|
||||
$(BUILD_DIR)/noise-suppressor-worklet.min.js \
|
||||
$(BUILD_DIR)/noise-suppressor-worklet.min.js.map \
|
||||
$(BUILD_DIR)/screenshot-capture-worker.min.js \
|
||||
$(BUILD_DIR)/screenshot-capture-worker.min.js.map \
|
||||
$(DEPLOY_DIR)
|
||||
cp \
|
||||
$(BUILD_DIR)/close3.min.js \
|
||||
|
||||
@@ -26,5 +26,5 @@ android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
android.bundle.enableUncompressedNativeLibs=false
|
||||
|
||||
appVersion=23.6.0
|
||||
sdkVersion=8.6.0
|
||||
appVersion=99.0.0
|
||||
sdkVersion=99.0.0
|
||||
|
||||
@@ -1061,13 +1061,6 @@ export default {
|
||||
return room.getSpeakerStats();
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the connection times stored in the library.
|
||||
*/
|
||||
getConnectionTimes() {
|
||||
return room.getConnectionTimes();
|
||||
},
|
||||
|
||||
// used by torture currently
|
||||
isJoined() {
|
||||
return room && room.isJoined();
|
||||
@@ -2445,7 +2438,7 @@ export default {
|
||||
* @param {string} [hangupReason] the reason for leaving the meeting
|
||||
* requested
|
||||
*/
|
||||
async hangup(requestFeedback = false, hangupReason) {
|
||||
hangup(requestFeedback = false, hangupReason) {
|
||||
APP.store.dispatch(disableReceiver());
|
||||
|
||||
this._stopProxyConnection();
|
||||
@@ -2462,33 +2455,42 @@ export default {
|
||||
|
||||
APP.UI.removeAllListeners();
|
||||
|
||||
let feedbackResult = {};
|
||||
let feedbackResultPromise = Promise.resolve({});
|
||||
|
||||
if (requestFeedback) {
|
||||
try {
|
||||
feedbackResult = await APP.store.dispatch(maybeOpenFeedbackDialog(room, hangupReason));
|
||||
} catch (err) { // eslint-disable-line no-empty
|
||||
const feedbackDialogClosed = (feedbackResult = {}) => {
|
||||
if (!feedbackResult.wasDialogShown && hangupReason) {
|
||||
return APP.store.dispatch(
|
||||
openLeaveReasonDialog(hangupReason)).then(() => feedbackResult);
|
||||
}
|
||||
|
||||
return Promise.resolve(feedbackResult);
|
||||
};
|
||||
|
||||
feedbackResultPromise
|
||||
= APP.store.dispatch(maybeOpenFeedbackDialog(room, hangupReason))
|
||||
.then(feedbackDialogClosed, feedbackDialogClosed);
|
||||
}
|
||||
|
||||
const leavePromise = this.leaveRoom().catch(() => Promise.resolve());
|
||||
|
||||
Promise.allSettled([ feedbackResultPromise, leavePromise ]).then(([ feedback, _ ]) => {
|
||||
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 anyway.
|
||||
*/
|
||||
if (!interfaceConfig.SHOW_PROMOTIONAL_CLOSE_PAGE) {
|
||||
APP.API.notifyReadyToClose();
|
||||
}
|
||||
}
|
||||
|
||||
if (!feedbackResult.wasDialogShown && hangupReason) {
|
||||
await APP.store.dispatch(openLeaveReasonDialog(hangupReason));
|
||||
}
|
||||
APP.store.dispatch(maybeRedirectToWelcomePage(feedback.value ?? {}));
|
||||
});
|
||||
|
||||
await this.leaveRoom();
|
||||
|
||||
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 anyway.
|
||||
*/
|
||||
if (!interfaceConfig.SHOW_PROMOTIONAL_CLOSE_PAGE) {
|
||||
APP.API.notifyReadyToClose();
|
||||
}
|
||||
APP.store.dispatch(maybeRedirectToWelcomePage(feedbackResult));
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -2498,7 +2500,7 @@ export default {
|
||||
* @param {string} reason - reason for leaving the room.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async leaveRoom(doDisconnect = true, reason = '') {
|
||||
leaveRoom(doDisconnect = true, reason = '') {
|
||||
APP.store.dispatch(conferenceWillLeave(room));
|
||||
|
||||
const maybeDisconnect = () => {
|
||||
|
||||
@@ -218,6 +218,9 @@ var config = {
|
||||
|
||||
// Video
|
||||
|
||||
// Sets the default camera facing mode.
|
||||
// cameraFacingMode: 'user',
|
||||
|
||||
// Sets the preferred resolution (height) for local video. Defaults to 720.
|
||||
// resolution: 720,
|
||||
|
||||
|
||||
@@ -61,6 +61,35 @@ body.welcome-page {
|
||||
|
||||
}
|
||||
|
||||
.not-allow-title-character-div {
|
||||
color: #f03e3e;
|
||||
background-color: #fff;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
margin: 10px 0px 5px 0px;
|
||||
text-align: $welcomePageHeaderTextAlign;
|
||||
border-radius: 5px;
|
||||
padding: 5px;
|
||||
.not-allow-title-character-text {
|
||||
float: right;
|
||||
line-height: 1.9;
|
||||
};
|
||||
.jitsi-icon {
|
||||
margin-right: 9px;
|
||||
float: left;
|
||||
|
||||
|
||||
svg {
|
||||
fill:#f03e3e;
|
||||
|
||||
& > *:first-child {
|
||||
fill: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.insecure-room-name-warning {
|
||||
align-items: center;
|
||||
color: rgb(215, 121, 118);
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>23.6.0</string>
|
||||
<string>99.0.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>NSExtension</key>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>23.6.0</string>
|
||||
<string>99.0.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>23.6.0</string>
|
||||
<string>99.0.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XPC!</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>23.6.0</string>
|
||||
<string>99.0.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>CLKComplicationPrincipalClass</key>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>8.6.0</string>
|
||||
<string>99.0.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>8.6.0</string>
|
||||
<string>99.0.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
|
||||
@@ -1107,7 +1107,11 @@ class API {
|
||||
this._enabled = true;
|
||||
|
||||
initCommands();
|
||||
|
||||
this.notifyBrowserSupport(isSupportedBrowser());
|
||||
|
||||
// Let the embedder know we are ready.
|
||||
this._sendEvent({ name: 'ready' });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1983,6 +1987,20 @@ class API {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify external application (if API is enabled) that the user received
|
||||
* a transcription chunk.
|
||||
*
|
||||
* @param {Object} data - The event data.
|
||||
* @returns {void}
|
||||
*/
|
||||
notifyTranscriptionChunkReceived(data) {
|
||||
this._sendEvent({
|
||||
name: 'transcription-chunk-received',
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify external application (if API is enabled) whether the used browser is supported or not.
|
||||
*
|
||||
|
||||
28
modules/API/external/external_api.js
vendored
28
modules/API/external/external_api.js
vendored
@@ -145,6 +145,7 @@ const events = {
|
||||
'prejoin-screen-loaded': 'prejoinScreenLoaded',
|
||||
'proxy-connection-event': 'proxyConnectionEvent',
|
||||
'raise-hand-updated': 'raiseHandUpdated',
|
||||
'ready': 'ready',
|
||||
'recording-link-available': 'recordingLinkAvailable',
|
||||
'recording-status-changed': 'recordingStatusChanged',
|
||||
'participant-menu-button-clicked': 'participantMenuButtonClick',
|
||||
@@ -159,6 +160,7 @@ const events = {
|
||||
'suspend-detected': 'suspendDetected',
|
||||
'tile-view-changed': 'tileViewChanged',
|
||||
'toolbar-button-clicked': 'toolbarButtonClicked',
|
||||
'transcription-chunk-received': 'transcriptionChunkReceived',
|
||||
'whiteboard-status-changed': 'whiteboardStatusChanged'
|
||||
};
|
||||
|
||||
@@ -273,10 +275,10 @@ function parseArguments(args) {
|
||||
function parseSizeParam(value) {
|
||||
let parsedValue;
|
||||
|
||||
// This regex parses values of the form 100px, 100em, 100pt or 100%.
|
||||
// This regex parses values of the form 100px, 100em, 100pt, 100vh, 100vw or 100%.
|
||||
// Values like 100 or 100px are handled outside of the regex, and
|
||||
// invalid values will be ignored and the minimum will be used.
|
||||
const re = /([0-9]*\.?[0-9]+)(em|pt|px|%)$/;
|
||||
const re = /([0-9]*\.?[0-9]+)(em|pt|px|((d|l|s)?v)(h|w)|%)$/;
|
||||
|
||||
if (typeof value === 'string' && String(value).match(re) !== null) {
|
||||
parsedValue = value;
|
||||
@@ -365,7 +367,9 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
|
||||
},
|
||||
release
|
||||
});
|
||||
this._createIFrame(height, width, onload, sandbox);
|
||||
|
||||
this._createIFrame(height, width, sandbox);
|
||||
|
||||
this._transport = new Transport({
|
||||
backend: new PostMessageTransportBackend({
|
||||
postisOptions: {
|
||||
@@ -375,9 +379,12 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
if (Array.isArray(invitees) && invitees.length > 0) {
|
||||
this.invite(invitees);
|
||||
}
|
||||
|
||||
this._onload = onload;
|
||||
this._tmpE2EEKey = e2eeKey;
|
||||
this._isLargeVideoVisible = false;
|
||||
this._isPrejoinVideoVisible = false;
|
||||
@@ -396,14 +403,12 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
|
||||
* parseSizeParam for format details.
|
||||
* @param {number|string} width - The with of the iframe. Check
|
||||
* parseSizeParam for format details.
|
||||
* @param {Function} onload - The function that will listen
|
||||
* for onload event.
|
||||
* @param {string} sandbox - Sandbox directive for the created iframe, if desired.
|
||||
* @returns {void}
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_createIFrame(height, width, onload, sandbox) {
|
||||
_createIFrame(height, width, sandbox) {
|
||||
const frameName = `jitsiConferenceFrame${id}`;
|
||||
|
||||
this._frame = document.createElement('iframe');
|
||||
@@ -427,11 +432,6 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
|
||||
this._frame.sandbox = sandbox;
|
||||
}
|
||||
|
||||
if (onload) {
|
||||
// waits for iframe resources to load
|
||||
// and fires event when it is done
|
||||
this._frame.onload = onload;
|
||||
}
|
||||
this._frame.src = this._url;
|
||||
|
||||
this._frame = this._parentNode.appendChild(this._frame);
|
||||
@@ -580,6 +580,12 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
|
||||
const userID = data.id;
|
||||
|
||||
switch (name) {
|
||||
case 'ready': {
|
||||
// Fake the iframe onload event because it's not reliable.
|
||||
this._onload?.();
|
||||
|
||||
break;
|
||||
}
|
||||
case 'video-conference-joined': {
|
||||
if (typeof this._tmpE2EEKey !== 'undefined') {
|
||||
|
||||
|
||||
687
package-lock.json
generated
687
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -65,12 +65,13 @@
|
||||
"js-md5": "0.6.1",
|
||||
"js-sha512": "0.8.0",
|
||||
"jwt-decode": "2.2.0",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1716.0.0+93c167d3/lib-jitsi-meet.tgz",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1720.0.0+b3173832/lib-jitsi-meet.tgz",
|
||||
"lodash": "4.17.21",
|
||||
"moment": "2.29.4",
|
||||
"moment-duration-format": "2.2.2",
|
||||
"null-loader": "4.0.1",
|
||||
"optional-require": "1.0.3",
|
||||
"pixelmatch": "5.3.0",
|
||||
"promise.allsettled": "1.0.4",
|
||||
"punycode": "2.3.0",
|
||||
"react": "18.2.0",
|
||||
@@ -113,7 +114,6 @@
|
||||
"react-youtube": "10.1.0",
|
||||
"redux": "4.0.4",
|
||||
"redux-thunk": "2.4.1",
|
||||
"resemblejs": "4.0.0",
|
||||
"seamless-scroll-polyfill": "2.1.8",
|
||||
"semver": "7.5.4",
|
||||
"tss-react": "4.4.4",
|
||||
@@ -136,6 +136,8 @@
|
||||
"@types/dom-screen-wake-lock": "1.0.1",
|
||||
"@types/js-md5": "0.4.3",
|
||||
"@types/lodash": "4.14.182",
|
||||
"@types/offscreencanvas": "2019.7.2",
|
||||
"@types/pixelmatch": "5.2.5",
|
||||
"@types/punycode": "2.1.0",
|
||||
"@types/react": "17.0.14",
|
||||
"@types/react-dom": "17.0.14",
|
||||
@@ -145,7 +147,6 @@
|
||||
"@types/react-native-video": "5.0.14",
|
||||
"@types/react-redux": "7.1.24",
|
||||
"@types/react-window": "1.8.5",
|
||||
"@types/resemblejs": "^4.1.0",
|
||||
"@types/unorm": "1.3.28",
|
||||
"@types/uuid": "8.3.4",
|
||||
"@types/w3c-image-capture": "1.0.6",
|
||||
|
||||
@@ -19,8 +19,6 @@ import { setAudioMuted, setVideoMuted } from './react/features/base/media/action
|
||||
|
||||
|
||||
interface IEventListeners {
|
||||
onAudioMutedChanged?: Function;
|
||||
onVideoMutedChanged?: Function;
|
||||
onConferenceBlurred?: Function;
|
||||
onConferenceFocused?: Function;
|
||||
onConferenceJoined?: Function;
|
||||
@@ -109,8 +107,6 @@ export const JitsiMeeting = forwardRef((props: IAppProps, ref) => {
|
||||
setAppProps({
|
||||
'flags': flags,
|
||||
'rnSdkHandlers': {
|
||||
onAudioMutedChanged: eventListeners?.onAudioMutedChanged,
|
||||
onVideoMutedChanged: eventListeners?.onVideoMutedChanged,
|
||||
onConferenceBlurred: eventListeners?.onConferenceBlurred,
|
||||
onConferenceFocused: eventListeners?.onConferenceFocused,
|
||||
onConferenceJoined: eventListeners?.onConferenceJoined,
|
||||
|
||||
@@ -5,15 +5,12 @@ import { openTokenAuthUrl } from '../authentication/actions';
|
||||
// @ts-ignore
|
||||
import { getTokenAuthUrl, isTokenAuthEnabled } from '../authentication/functions';
|
||||
import { getJwtExpirationDate } from '../base/jwt/functions';
|
||||
import { MEDIA_TYPE } from '../base/media/constants';
|
||||
import { isLocalTrackMuted } from '../base/tracks/functions.any';
|
||||
import { getLocationContextRoot, parseURIString } from '../base/util/uri';
|
||||
|
||||
import { addTrackStateToURL } from './functions.any';
|
||||
import logger from './logger';
|
||||
import { IStore } from './types';
|
||||
|
||||
|
||||
/**
|
||||
* Redirects to another page generated by replacing the path in the original URL
|
||||
* with the given path.
|
||||
@@ -107,11 +104,7 @@ export function maybeRedirectToTokenAuthUrl(
|
||||
dispatch: IStore['dispatch'], getState: IStore['getState'], failureCallback: Function) {
|
||||
const state = getState();
|
||||
const config = state['features/base/config'];
|
||||
const { enabled: audioOnlyEnabled } = state['features/base/audio-only'];
|
||||
const { startAudioOnly } = config;
|
||||
const { locationURL = { href: '' } as URL } = state['features/base/connection'];
|
||||
const audioMuted = isLocalTrackMuted(state['features/base/tracks'], MEDIA_TYPE.AUDIO);
|
||||
const videoMuted = isLocalTrackMuted(state['features/base/tracks'], MEDIA_TYPE.VIDEO);
|
||||
|
||||
if (!isTokenAuthEnabled(config)) {
|
||||
return false;
|
||||
@@ -127,18 +120,7 @@ export function maybeRedirectToTokenAuthUrl(
|
||||
const room = state['features/base/conference'].room;
|
||||
const { tenant } = parseURIString(locationURL.href) || {};
|
||||
|
||||
getTokenAuthUrl(
|
||||
config,
|
||||
locationURL,
|
||||
{
|
||||
audioMuted,
|
||||
audioOnlyEnabled: audioOnlyEnabled || startAudioOnly,
|
||||
skipPrejoin: true,
|
||||
videoMuted
|
||||
},
|
||||
room,
|
||||
tenant
|
||||
)
|
||||
getTokenAuthUrl(config, room, tenant, true, locationURL)
|
||||
.then((tokenAuthServiceUrl: string | undefined) => {
|
||||
if (!tokenAuthServiceUrl) {
|
||||
logger.warn('Cannot handle login, token service URL is not set');
|
||||
|
||||
@@ -55,20 +55,8 @@ function _getWebConferenceRoute(state: IReduxState) {
|
||||
&& !state['features/base/jwt'].jwt && room) {
|
||||
const { locationURL = { href: '' } as URL } = state['features/base/connection'];
|
||||
const { tenant } = parseURIString(locationURL.href) || {};
|
||||
const { startAudioOnly } = config;
|
||||
|
||||
return getTokenAuthUrl(
|
||||
config,
|
||||
locationURL,
|
||||
{
|
||||
audioMuted: false,
|
||||
audioOnlyEnabled: startAudioOnly,
|
||||
skipPrejoin: false,
|
||||
videoMuted: false
|
||||
},
|
||||
room,
|
||||
tenant
|
||||
)
|
||||
return getTokenAuthUrl(config, room, tenant, false, locationURL)
|
||||
.then((url: string | undefined) => {
|
||||
route.href = url;
|
||||
|
||||
|
||||
@@ -13,65 +13,29 @@ export const isTokenAuthEnabled = (config: IConfig): boolean =>
|
||||
/**
|
||||
* Returns the state that we can add as a parameter to the tokenAuthUrl.
|
||||
*
|
||||
* @param {URL} locationURL - The location URL.
|
||||
* @param {Object} options: - Config options {
|
||||
* audioMuted: boolean | undefined
|
||||
* audioOnlyEnabled: boolean | undefined,
|
||||
* skipPrejoin: boolean | undefined,
|
||||
* videoMuted: boolean | undefined
|
||||
* }.
|
||||
* @param {string?} roomName - The room name.
|
||||
* @param {string?} tenant - The tenant name if any.
|
||||
*
|
||||
* @param {boolean} skipPrejoin - Whether to skip pre-join page.
|
||||
* @param {URL} locationURL - The location URL.
|
||||
* @returns {Object} The state object.
|
||||
*/
|
||||
export const _getTokenAuthState = (
|
||||
locationURL: URL,
|
||||
options: {
|
||||
audioMuted: boolean | undefined;
|
||||
audioOnlyEnabled: boolean | undefined;
|
||||
skipPrejoin: boolean | undefined;
|
||||
videoMuted: boolean | undefined;
|
||||
},
|
||||
roomName: string | undefined,
|
||||
tenant: string | undefined): object => {
|
||||
tenant: string | undefined,
|
||||
skipPrejoin: boolean | undefined = false,
|
||||
locationURL: URL): object => {
|
||||
const state = {
|
||||
room: roomName,
|
||||
roomSafe: getBackendSafeRoomName(roomName),
|
||||
tenant
|
||||
};
|
||||
|
||||
const {
|
||||
audioMuted = false,
|
||||
audioOnlyEnabled = false,
|
||||
skipPrejoin = false,
|
||||
videoMuted = false
|
||||
} = options;
|
||||
|
||||
if (audioMuted) {
|
||||
|
||||
// @ts-ignore
|
||||
state['config.startWithAudioMuted'] = true;
|
||||
}
|
||||
|
||||
if (audioOnlyEnabled) {
|
||||
|
||||
// @ts-ignore
|
||||
state['config.startAudioOnly'] = true;
|
||||
}
|
||||
|
||||
if (skipPrejoin) {
|
||||
// We have already shown the prejoin screen, no need to show it again after obtaining the token.
|
||||
// @ts-ignore
|
||||
state['config.prejoinConfig.enabled'] = false;
|
||||
}
|
||||
|
||||
if (videoMuted) {
|
||||
|
||||
// @ts-ignore
|
||||
state['config.startWithVideoMuted'] = true;
|
||||
}
|
||||
|
||||
const params = new URLSearchParams(locationURL.hash);
|
||||
|
||||
for (const [ key, value ] of params) {
|
||||
|
||||
@@ -14,15 +14,10 @@ export * from './functions.any';
|
||||
* argument to this method.
|
||||
*
|
||||
* @param {Object} config - Configuration state object from store. A URL pattern pointing to the login service.
|
||||
* @param {string} roomName - The name of the conference room for which the user will be authenticated.
|
||||
* @param {string} tenant - The name of the conference tenant.
|
||||
* @param {string} skipPrejoin - The name of the conference room for which the user will be authenticated.
|
||||
* @param {URL} locationURL - The location URL.
|
||||
* @param {Object} options: - Config options {
|
||||
* audioMuted: boolean | undefined
|
||||
* audioOnlyEnabled: boolean | undefined,
|
||||
* skipPrejoin: boolean | undefined,
|
||||
* videoMuted: boolean | undefined
|
||||
* }.
|
||||
* @param {string?} roomName - The room name.
|
||||
* @param {string?} tenant - The tenant name if any.
|
||||
*
|
||||
* @returns {Promise<string|undefined>} - The URL pointing to JWT login service or
|
||||
* <tt>undefined</tt> if the pattern stored in config is not a string and the URL can not be
|
||||
@@ -30,23 +25,11 @@ export * from './functions.any';
|
||||
*/
|
||||
export const getTokenAuthUrl = (
|
||||
config: IConfig,
|
||||
locationURL: URL,
|
||||
options: {
|
||||
audioMuted: boolean | undefined;
|
||||
audioOnlyEnabled: boolean | undefined;
|
||||
skipPrejoin: boolean | undefined;
|
||||
videoMuted: boolean | undefined;
|
||||
},
|
||||
roomName: string | undefined,
|
||||
tenant: string | undefined,
|
||||
skipPrejoin: boolean | undefined = false,
|
||||
// eslint-disable-next-line max-params
|
||||
tenant: string | undefined): Promise<string | undefined> => {
|
||||
|
||||
const {
|
||||
audioMuted = false,
|
||||
audioOnlyEnabled = false,
|
||||
skipPrejoin = false,
|
||||
videoMuted = false
|
||||
} = options;
|
||||
locationURL: URL): Promise<string | undefined> => {
|
||||
|
||||
let url = config.tokenAuthUrl;
|
||||
|
||||
@@ -55,17 +38,7 @@ export const getTokenAuthUrl = (
|
||||
}
|
||||
|
||||
if (url.indexOf('{state}')) {
|
||||
const state = _getTokenAuthState(
|
||||
locationURL,
|
||||
{
|
||||
audioMuted,
|
||||
audioOnlyEnabled,
|
||||
skipPrejoin,
|
||||
videoMuted
|
||||
},
|
||||
roomName,
|
||||
tenant
|
||||
);
|
||||
const state = _getTokenAuthState(roomName, tenant, skipPrejoin, locationURL);
|
||||
|
||||
// Append ios=true or android=true to the token URL.
|
||||
// @ts-ignore
|
||||
|
||||
@@ -32,15 +32,10 @@ function _cryptoRandom() {
|
||||
* argument to this method.
|
||||
*
|
||||
* @param {Object} config - Configuration state object from store. A URL pattern pointing to the login service.
|
||||
* @param {URL} locationURL - The location URL.
|
||||
* @param {Object} options: - Config options {
|
||||
* audioMuted: boolean | undefined
|
||||
* audioOnlyEnabled: boolean | undefined,
|
||||
* skipPrejoin: boolean | undefined,
|
||||
* videoMuted: boolean | undefined
|
||||
* }.
|
||||
* @param {string?} roomName - The room name.
|
||||
* @param {string?} tenant - The tenant name if any.
|
||||
* @param {string} roomName - The name of the conference room for which the user will be authenticated.
|
||||
* @param {string} tenant - The name of the conference tenant.
|
||||
* @param {string} skipPrejoin - The name of the conference room for which the user will be authenticated.
|
||||
* @param {URL} locationURL - The current location URL.
|
||||
*
|
||||
* @returns {Promise<string|undefined>} - The URL pointing to JWT login service or
|
||||
* <tt>undefined</tt> if the pattern stored in config is not a string and the URL can not be
|
||||
@@ -48,23 +43,11 @@ function _cryptoRandom() {
|
||||
*/
|
||||
export const getTokenAuthUrl = (
|
||||
config: IConfig,
|
||||
locationURL: URL,
|
||||
options: {
|
||||
audioMuted: boolean | undefined;
|
||||
audioOnlyEnabled: boolean | undefined;
|
||||
skipPrejoin: boolean | undefined;
|
||||
videoMuted: boolean | undefined;
|
||||
},
|
||||
roomName: string | undefined,
|
||||
tenant: string | undefined,
|
||||
skipPrejoin: boolean | undefined = false,
|
||||
// eslint-disable-next-line max-params
|
||||
tenant: string | undefined): Promise<string | undefined> => {
|
||||
|
||||
const {
|
||||
audioMuted = false,
|
||||
audioOnlyEnabled = false,
|
||||
skipPrejoin = false,
|
||||
videoMuted = false
|
||||
} = options;
|
||||
locationURL: URL): Promise<string | undefined> => {
|
||||
|
||||
let url = config.tokenAuthUrl;
|
||||
|
||||
@@ -73,17 +56,7 @@ export const getTokenAuthUrl = (
|
||||
}
|
||||
|
||||
if (url.indexOf('{state}')) {
|
||||
const state = _getTokenAuthState(
|
||||
locationURL,
|
||||
{
|
||||
audioMuted,
|
||||
audioOnlyEnabled,
|
||||
skipPrejoin,
|
||||
videoMuted
|
||||
},
|
||||
roomName,
|
||||
tenant
|
||||
);
|
||||
const state = _getTokenAuthState(roomName, tenant, skipPrejoin, locationURL);
|
||||
|
||||
if (browser.isElectron()) {
|
||||
// @ts-ignore
|
||||
|
||||
@@ -13,9 +13,7 @@ import {
|
||||
JitsiConferenceErrors,
|
||||
JitsiConnectionErrors
|
||||
} from '../base/lib-jitsi-meet';
|
||||
import { MEDIA_TYPE } from '../base/media/constants';
|
||||
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
|
||||
import { isLocalTrackMuted } from '../base/tracks/functions.any';
|
||||
import { parseURIString } from '../base/util/uri';
|
||||
import { openLogoutDialog } from '../settings/actions';
|
||||
|
||||
@@ -259,9 +257,6 @@ function _handleLogin({ dispatch, getState }: IStore) {
|
||||
const room = state['features/base/conference'].room;
|
||||
const { locationURL = { href: '' } as URL } = state['features/base/connection'];
|
||||
const { tenant } = parseURIString(locationURL.href) || {};
|
||||
const { enabled: audioOnlyEnabled } = state['features/base/audio-only'];
|
||||
const audioMuted = isLocalTrackMuted(state['features/base/tracks'], MEDIA_TYPE.AUDIO);
|
||||
const videoMuted = isLocalTrackMuted(state['features/base/tracks'], MEDIA_TYPE.VIDEO);
|
||||
|
||||
if (!room) {
|
||||
logger.warn('Cannot handle login, room is undefined!');
|
||||
@@ -275,18 +270,7 @@ function _handleLogin({ dispatch, getState }: IStore) {
|
||||
return;
|
||||
}
|
||||
|
||||
getTokenAuthUrl(
|
||||
config,
|
||||
locationURL,
|
||||
{
|
||||
audioMuted,
|
||||
audioOnlyEnabled,
|
||||
skipPrejoin: true,
|
||||
videoMuted
|
||||
},
|
||||
room,
|
||||
tenant
|
||||
)
|
||||
getTokenAuthUrl(config, room, tenant, true, locationURL)
|
||||
.then((tokenAuthServiceUrl: string | undefined) => {
|
||||
if (!tokenAuthServiceUrl) {
|
||||
logger.warn('Cannot handle login, token service URL is not set');
|
||||
|
||||
@@ -253,6 +253,7 @@ export interface IConfig {
|
||||
callStatsID?: string;
|
||||
callStatsSecret?: string;
|
||||
callUUID?: string;
|
||||
cameraFacingMode?: string;
|
||||
channelLastN?: number;
|
||||
chromeExtensionBanner?: {
|
||||
chromeExtensionsInfo?: Array<{ id: string; path: string; }>;
|
||||
|
||||
@@ -74,6 +74,7 @@ export default [
|
||||
*/
|
||||
'callUUID',
|
||||
|
||||
'cameraFacingMode',
|
||||
'conferenceInfo',
|
||||
'channelLastN',
|
||||
'connectionIndicators',
|
||||
|
||||
@@ -23,7 +23,12 @@ import MiddlewareRegistry from '../redux/MiddlewareRegistry';
|
||||
import { getPropertyValue } from '../settings/functions.any';
|
||||
import { TRACK_ADDED } from '../tracks/actionTypes';
|
||||
import { destroyLocalTracks } from '../tracks/actions.any';
|
||||
import { isLocalTrackMuted, isLocalVideoTrackDesktop, setTrackMuted } from '../tracks/functions.any';
|
||||
import {
|
||||
getCameraFacingMode,
|
||||
isLocalTrackMuted,
|
||||
isLocalVideoTrackDesktop,
|
||||
setTrackMuted
|
||||
} from '../tracks/functions.any';
|
||||
import { ITrack } from '../tracks/types';
|
||||
|
||||
import {
|
||||
@@ -40,7 +45,6 @@ import {
|
||||
setVideoMuted
|
||||
} from './actions';
|
||||
import {
|
||||
CAMERA_FACING_MODE,
|
||||
MEDIA_TYPE,
|
||||
SCREENSHARE_MUTISM_AUTHORITY,
|
||||
VIDEO_MUTISM_AUTHORITY
|
||||
@@ -233,7 +237,7 @@ function _setRoom({ dispatch, getState }: IStore, next: Function, action: AnyAct
|
||||
// the user i.e. the state of base/media. Eventually, practice/reality i.e.
|
||||
// the state of base/tracks will or will not agree with the desires.
|
||||
dispatch(setAudioMuted(audioMuted));
|
||||
dispatch(setCameraFacingMode(CAMERA_FACING_MODE.USER));
|
||||
dispatch(setCameraFacingMode(getCameraFacingMode(state)));
|
||||
dispatch(setVideoMuted(videoMuted));
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,11 @@ export interface IProps extends WithTranslation {
|
||||
*/
|
||||
handleClick?: Function;
|
||||
|
||||
/**
|
||||
* Whether the button open a menu or not.
|
||||
*/
|
||||
isMenuButton?: boolean;
|
||||
|
||||
/**
|
||||
* Notify mode for `toolbarButtonClicked` event -
|
||||
* whether to only notify or to also prevent button click routine.
|
||||
@@ -102,7 +107,7 @@ export const defaultDisabledButtonStyles = {
|
||||
/**
|
||||
* An abstract implementation of a button.
|
||||
*/
|
||||
export default class AbstractButton<P extends IProps, S=any> extends Component<P, S> {
|
||||
export default class AbstractButton<P extends IProps, S = any> extends Component<P, S> {
|
||||
static defaultProps = {
|
||||
afterClick: undefined,
|
||||
disabledStyles: defaultDisabledButtonStyles,
|
||||
@@ -354,7 +359,7 @@ export default class AbstractButton<P extends IProps, S=any> extends Component<P
|
||||
|
||||
if (typeof APP !== 'undefined' && notifyMode) {
|
||||
APP.API.notifyToolbarButtonClicked(
|
||||
buttonKey, notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY
|
||||
buttonKey, notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,11 @@ interface IProps extends AbstractToolboxItemProps {
|
||||
*/
|
||||
contextMenu?: boolean;
|
||||
|
||||
/**
|
||||
* Whether the button open a menu or not.
|
||||
*/
|
||||
isMenuButton?: boolean;
|
||||
|
||||
/**
|
||||
* On key down handler.
|
||||
*/
|
||||
@@ -67,6 +72,7 @@ export default class ToolboxItem extends AbstractToolboxItem<IProps> {
|
||||
const {
|
||||
backgroundColor,
|
||||
contextMenu,
|
||||
isMenuButton,
|
||||
disabled,
|
||||
elementAfter,
|
||||
icon,
|
||||
@@ -77,8 +83,9 @@ export default class ToolboxItem extends AbstractToolboxItem<IProps> {
|
||||
toggled
|
||||
} = this.props;
|
||||
const className = showLabel ? 'overflow-menu-item' : 'toolbox-button';
|
||||
const buttonAttribute = isMenuButton ? 'aria-expanded' : 'aria-pressed';
|
||||
const props = {
|
||||
'aria-pressed': toggled,
|
||||
[buttonAttribute]: toggled,
|
||||
'aria-disabled': disabled,
|
||||
'aria-label': this.accessibilityLabel,
|
||||
className: className + (disabled ? ' disabled' : ''),
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
} from './actionTypes';
|
||||
import {
|
||||
createLocalTracksF,
|
||||
getCameraFacingMode,
|
||||
getLocalTrack,
|
||||
getLocalTracks,
|
||||
getLocalVideoTrack,
|
||||
@@ -140,6 +141,7 @@ export function createLocalTracksA(options: ITrackOptions = {}) {
|
||||
getState
|
||||
};
|
||||
const promises = [];
|
||||
const state = getState();
|
||||
|
||||
// The following executes on React Native only at the time of this
|
||||
// writing. The effort to port Web's createInitialLocalTracks
|
||||
@@ -153,7 +155,7 @@ export function createLocalTracksA(options: ITrackOptions = {}) {
|
||||
// device separately.
|
||||
for (const device of devices) {
|
||||
if (getLocalTrack(
|
||||
getState()['features/base/tracks'],
|
||||
state['features/base/tracks'],
|
||||
device as MediaType,
|
||||
/* includePending */ true)) {
|
||||
throw new Error(`Local track for ${device} already exists`);
|
||||
@@ -165,7 +167,7 @@ export function createLocalTracksA(options: ITrackOptions = {}) {
|
||||
cameraDeviceId: options.cameraDeviceId,
|
||||
devices: [ device ],
|
||||
facingMode:
|
||||
options.facingMode || CAMERA_FACING_MODE.USER,
|
||||
options.facingMode || getCameraFacingMode(state),
|
||||
micDeviceId: options.micDeviceId
|
||||
},
|
||||
store)
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
} from '../config/functions.any';
|
||||
import { JitsiTrackErrors, browser } from '../lib-jitsi-meet';
|
||||
import { gumPending } from '../media/actions';
|
||||
import { MEDIA_TYPE, MediaType, VIDEO_TYPE } from '../media/constants';
|
||||
import { CAMERA_FACING_MODE, MEDIA_TYPE, MediaType, VIDEO_TYPE } from '../media/constants';
|
||||
import { IMediaState } from '../media/reducer';
|
||||
import { IGUMPendingState } from '../media/types';
|
||||
import {
|
||||
@@ -447,3 +447,13 @@ export function logTracksForParticipant(tracksState: ITrack[], participantId: st
|
||||
|
||||
logger.debug(`${logStringPrefix}${reason ? `(reason: ${reason})` : ''}:${tracksLogMsg}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the default camera facing mode.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {string} - The camera facing mode.
|
||||
*/
|
||||
export function getCameraFacingMode(state: IReduxState) {
|
||||
return state['features/base/config'].cameraFacingMode ?? CAMERA_FACING_MODE.USER;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { IStore } from '../../app/types';
|
||||
import JitsiMeetJS from '../lib-jitsi-meet';
|
||||
|
||||
import { getCameraFacingMode } from './functions.any';
|
||||
import { ITrackOptions } from './types';
|
||||
|
||||
export * from './functions.any';
|
||||
@@ -39,6 +40,7 @@ export function createLocalTracksF(options: ITrackOptions = {}, store: IStore) {
|
||||
|
||||
// Copy array to avoid mutations inside library.
|
||||
devices: options.devices?.slice(0),
|
||||
facingMode: options.facingMode || getCameraFacingMode(state),
|
||||
micDeviceId,
|
||||
resolution
|
||||
});
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
getUserSelectedMicDeviceId
|
||||
} from '../settings/functions.web';
|
||||
|
||||
import { getCameraFacingMode } from './functions.any';
|
||||
import loadEffects from './loadEffects';
|
||||
import logger from './logger';
|
||||
import { ITrackOptions } from './types';
|
||||
@@ -82,6 +83,7 @@ export function createLocalTracksF(options: ITrackOptions = {}, store?: IStore)
|
||||
// Copy array to avoid mutations inside library.
|
||||
devices: options.devices?.slice(0),
|
||||
effects,
|
||||
facingMode: options.facingMode || getCameraFacingMode(state),
|
||||
firefox_fake_device, // eslint-disable-line camelcase
|
||||
firePermissionPromptIsShownEvent,
|
||||
micDeviceId,
|
||||
|
||||
@@ -11,6 +11,7 @@ const button = {
|
||||
|
||||
const buttonLabel = {
|
||||
...BaseTheme.typography.bodyShortBold,
|
||||
lineHeight: 14,
|
||||
textTransform: 'capitalize'
|
||||
};
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
import { EdgeInsets, withSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { connect, useDispatch } from 'react-redux';
|
||||
|
||||
import { appNavigate } from '../../../app/actions.native';
|
||||
import { appNavigate } from '../../../app/actions';
|
||||
import { IReduxState, IStore } from '../../../app/types';
|
||||
import { CONFERENCE_BLURRED, CONFERENCE_FOCUSED } from '../../../base/conference/actionTypes';
|
||||
import { FULLSCREEN_ENABLED, PIP_ENABLED } from '../../../base/flags/constants';
|
||||
@@ -41,15 +41,15 @@ import { navigate } from '../../../mobile/navigation/components/conference/Confe
|
||||
import { screen } from '../../../mobile/navigation/routes';
|
||||
import { setPictureInPictureEnabled } from '../../../mobile/picture-in-picture/functions';
|
||||
import Captions from '../../../subtitles/components/native/Captions';
|
||||
import { setToolboxVisible } from '../../../toolbox/actions.native';
|
||||
import { setToolboxVisible } from '../../../toolbox/actions';
|
||||
import Toolbox from '../../../toolbox/components/native/Toolbox';
|
||||
import { isToolboxVisible } from '../../../toolbox/functions.native';
|
||||
import { isToolboxVisible } from '../../../toolbox/functions';
|
||||
import {
|
||||
AbstractConference,
|
||||
abstractMapStateToProps
|
||||
} from '../AbstractConference';
|
||||
import type { AbstractProps } from '../AbstractConference';
|
||||
import { isConnecting } from '../functions.native';
|
||||
import { isConnecting } from '../functions';
|
||||
|
||||
import AlwaysOnLabels from './AlwaysOnLabels';
|
||||
import ExpandedLabelPopup from './ExpandedLabelPopup';
|
||||
@@ -230,9 +230,7 @@ class Conference extends AbstractConference<IProps, State> {
|
||||
*/
|
||||
componentDidUpdate(prevProps: IProps) {
|
||||
const {
|
||||
_audioOnlyEnabled,
|
||||
_showLobby,
|
||||
_startCarMode
|
||||
_showLobby
|
||||
} = this.props;
|
||||
|
||||
if (!prevProps._showLobby && _showLobby) {
|
||||
@@ -240,10 +238,6 @@ class Conference extends AbstractConference<IProps, State> {
|
||||
}
|
||||
|
||||
if (prevProps._showLobby && !_showLobby) {
|
||||
if (_audioOnlyEnabled && _startCarMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
navigate(screen.conference.main);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Component } from 'react';
|
||||
import { createInviteDialogEvent } from '../../../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../../../analytics/functions';
|
||||
import { IReduxState, IStore } from '../../../app/types';
|
||||
import { getMeetingRegion } from '../../../base/config/functions.any';
|
||||
import { showErrorNotification, showNotification } from '../../../notifications/actions';
|
||||
import { NOTIFICATION_TIMEOUT_TYPE } from '../../../notifications/constants';
|
||||
import { INotificationProps } from '../../../notifications/types';
|
||||
@@ -65,6 +66,11 @@ export interface IProps {
|
||||
*/
|
||||
_peopleSearchUrl: string;
|
||||
|
||||
/**
|
||||
* The region where we connected to.
|
||||
*/
|
||||
_region: string;
|
||||
|
||||
/**
|
||||
* Whether or not to allow sip invites.
|
||||
*/
|
||||
@@ -248,6 +254,7 @@ export default class AbstractAddPeopleDialog<P extends IProps, S extends IState>
|
||||
_jwt: jwt,
|
||||
_peopleSearchQueryTypes: peopleSearchQueryTypes,
|
||||
_peopleSearchUrl: peopleSearchUrl,
|
||||
_region: region,
|
||||
_sipInviteEnabled: sipInviteEnabled
|
||||
} = this.props;
|
||||
const options = {
|
||||
@@ -259,6 +266,7 @@ export default class AbstractAddPeopleDialog<P extends IProps, S extends IState>
|
||||
jwt,
|
||||
peopleSearchQueryTypes,
|
||||
peopleSearchUrl,
|
||||
region,
|
||||
sipInviteEnabled
|
||||
};
|
||||
|
||||
@@ -300,6 +308,7 @@ export function _mapStateToProps(state: IReduxState) {
|
||||
_jwt: state['features/base/jwt'].jwt ?? '',
|
||||
_peopleSearchQueryTypes: peopleSearchQueryTypes ?? [],
|
||||
_peopleSearchUrl: peopleSearchUrl ?? '',
|
||||
_region: getMeetingRegion(state),
|
||||
_sipInviteEnabled: isSipInviteEnabled(state)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -41,13 +41,15 @@ export const sharingFeatures = {
|
||||
*
|
||||
* @param {string} dialNumber - The dial number to check for validity.
|
||||
* @param {string} dialOutAuthUrl - The endpoint to use for checking validity.
|
||||
* @param {string} region - The region we are connected to.
|
||||
* @returns {Promise} - The promise created by the request.
|
||||
*/
|
||||
export function checkDialNumber(
|
||||
dialNumber: string,
|
||||
dialOutAuthUrl: string
|
||||
dialOutAuthUrl: string,
|
||||
region: string
|
||||
): Promise<{ allow?: boolean; country?: string; phone?: string; }> {
|
||||
const fullUrl = `${dialOutAuthUrl}?phone=${dialNumber}`;
|
||||
const fullUrl = `${dialOutAuthUrl}?phone=${dialNumber}®ion=${region}`;
|
||||
|
||||
return new Promise((resolve, reject) =>
|
||||
fetch(fullUrl)
|
||||
@@ -146,6 +148,11 @@ export type GetInviteResultsOptions = {
|
||||
*/
|
||||
peopleSearchUrl: string;
|
||||
|
||||
/**
|
||||
* The region we are connected to.
|
||||
*/
|
||||
region: string;
|
||||
|
||||
/**
|
||||
* Whether or not to check sip invites.
|
||||
*/
|
||||
@@ -174,6 +181,7 @@ export function getInviteResultsForQuery(
|
||||
dialOutEnabled,
|
||||
peopleSearchQueryTypes,
|
||||
peopleSearchUrl,
|
||||
region,
|
||||
sipInviteEnabled,
|
||||
jwt
|
||||
} = options;
|
||||
@@ -217,7 +225,7 @@ export function getInviteResultsForQuery(
|
||||
// so ensure only digits get sent.
|
||||
numberToVerify = getDigitsOnly(numberToVerify);
|
||||
|
||||
phoneNumberPromise = checkDialNumber(numberToVerify, dialOutAuthUrl);
|
||||
phoneNumberPromise = checkDialNumber(numberToVerify, dialOutAuthUrl, region);
|
||||
} else if (dialOutEnabled && !dialOutAuthUrl) {
|
||||
// fake having a country code to hide the country code reminder
|
||||
hasCountryCode = true;
|
||||
|
||||
@@ -228,16 +228,17 @@ class LargeVideo extends Component<IProps> {
|
||||
* another container for the background and the
|
||||
* largeVideoWrapper in order to hide/show them.
|
||||
*/}
|
||||
{ _displayScreenSharingPlaceholder ? <ScreenSharePlaceholder /> : <></>}
|
||||
<div
|
||||
id = 'largeVideoWrapper'
|
||||
onTouchEnd = { this._onDoubleTap }
|
||||
ref = { this._wrapperRef }
|
||||
role = 'figure' >
|
||||
{ _displayScreenSharingPlaceholder ? <ScreenSharePlaceholder /> : <video
|
||||
<video
|
||||
autoPlay = { !_noAutoPlayVideo }
|
||||
id = 'largeVideo'
|
||||
muted = { true }
|
||||
playsInline = { true } /* for Safari on iOS to work */ /> }
|
||||
playsInline = { true } /* for Safari on iOS to work */ />
|
||||
</div>
|
||||
</div>
|
||||
{ interfaceConfig.DISABLE_TRANSCRIPTION_SUBTITLES
|
||||
|
||||
@@ -15,7 +15,10 @@ const useStyles = makeStyles()(theme => {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
position: 'absolute'
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
zIndex: 2
|
||||
},
|
||||
content: {
|
||||
display: 'flex',
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
CONFERENCE_LEFT,
|
||||
CONFERENCE_WILL_JOIN
|
||||
} from '../../base/conference/actionTypes';
|
||||
import { SET_AUDIO_MUTED, SET_VIDEO_MUTED } from '../../base/media/actionTypes';
|
||||
import { PARTICIPANT_JOINED } from '../../base/participants/actionTypes';
|
||||
import MiddlewareRegistry from '../../base/redux/MiddlewareRegistry';
|
||||
import StateListenerRegistry from '../../base/redux/StateListenerRegistry';
|
||||
@@ -32,12 +31,6 @@ const { JMOngoingConference } = NativeModules;
|
||||
const rnSdkHandlers = getAppProp(store, 'rnSdkHandlers');
|
||||
|
||||
switch (type) {
|
||||
case SET_AUDIO_MUTED:
|
||||
rnSdkHandlers?.onAudioMutedChanged && rnSdkHandlers?.onAudioMutedChanged(action.muted);
|
||||
break;
|
||||
case SET_VIDEO_MUTED:
|
||||
rnSdkHandlers?.onVideoMutedChanged && rnSdkHandlers?.onVideoMutedChanged(Boolean(action.muted));
|
||||
break;
|
||||
case CONFERENCE_BLURRED:
|
||||
rnSdkHandlers?.onConferenceBlurred && rnSdkHandlers?.onConferenceBlurred();
|
||||
break;
|
||||
|
||||
@@ -128,6 +128,7 @@ export const RoomParticipantContextMenu = ({
|
||||
size = { AVATAR_SIZE } />,
|
||||
text: entity?.participantName
|
||||
} ] } />}
|
||||
|
||||
<ContextMenuItemGroup>
|
||||
<div className = { styles.text }>
|
||||
{t('breakoutRooms.actions.sendToBreakoutRoom')}
|
||||
|
||||
@@ -6,6 +6,10 @@ import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { openDialog, openSheet } from '../../../base/dialog/actions';
|
||||
import {
|
||||
BREAKOUT_ROOMS_BUTTON_ENABLED
|
||||
} from '../../../base/flags/constants';
|
||||
import { getFeatureFlag } from '../../../base/flags/functions';
|
||||
import Icon from '../../../base/icons/components/Icon';
|
||||
import { IconDotsHorizontal, IconRingGroup } from '../../../base/icons/svg';
|
||||
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
|
||||
@@ -34,6 +38,9 @@ const ParticipantsPaneFooter = (): JSX.Element => {
|
||||
const isBreakoutRoomsSupported = useSelector((state: IReduxState) =>
|
||||
state['features/base/conference'].conference?.getBreakoutRooms()?.isSupported()
|
||||
);
|
||||
const isBreakoutRoomsEnabled = useSelector((state: IReduxState) =>
|
||||
getFeatureFlag(state, BREAKOUT_ROOMS_BUTTON_ENABLED, true)
|
||||
);
|
||||
const openMoreMenu = useCallback(() => dispatch(openSheet(ContextMenuMore)), [ dispatch ]);
|
||||
const muteAll = useCallback(() => dispatch(openDialog(MuteEveryoneDialog)),
|
||||
[ dispatch ]);
|
||||
@@ -44,6 +51,7 @@ const ParticipantsPaneFooter = (): JSX.Element => {
|
||||
<View style = { styles.participantsPaneFooterContainer as ViewStyle }>
|
||||
{
|
||||
isBreakoutRoomsSupported
|
||||
&& isBreakoutRoomsEnabled
|
||||
&& <Button
|
||||
accessibilityLabel = 'participantsPane.actions.breakoutRooms'
|
||||
// eslint-disable-next-line react/jsx-no-bind, no-confusing-arrow
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { FlatList, Text, TextStyle, View, ViewStyle } from 'react-native';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { getLocalParticipant } from '../../../base/participants/functions';
|
||||
import Button from '../../../base/ui/components/native/Button';
|
||||
import { BUTTON_TYPES } from '../../../base/ui/constants.native';
|
||||
import AbstractPollResults from '../AbstractPollResults';
|
||||
@@ -18,7 +20,6 @@ const PollResults = (props: AbstractProps) => {
|
||||
const {
|
||||
answers,
|
||||
changeVote,
|
||||
creatorName,
|
||||
haveVoted,
|
||||
question,
|
||||
showDetails,
|
||||
@@ -87,6 +88,7 @@ const PollResults = (props: AbstractProps) => {
|
||||
);
|
||||
|
||||
}, [ showDetails ]);
|
||||
const localParticipant = useSelector(getLocalParticipant);
|
||||
|
||||
|
||||
/* eslint-disable react/jsx-no-bind */
|
||||
@@ -94,7 +96,7 @@ const PollResults = (props: AbstractProps) => {
|
||||
<View>
|
||||
<Text style = { dialogStyles.questionText as TextStyle } >{ question }</Text>
|
||||
<Text style = { dialogStyles.questionOwnerText as TextStyle } >
|
||||
{ t('polls.by', { name: creatorName }) }
|
||||
{ t('polls.by', { name: localParticipant?.name }) }
|
||||
</Text>
|
||||
<FlatList
|
||||
data = { answers }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import resemble from 'resemblejs';
|
||||
import 'image-capture';
|
||||
import JitsiTrack from 'lib-jitsi-meet/types/auto/modules/RTC/JitsiTrack';
|
||||
import './createImageBitmap';
|
||||
|
||||
import { createScreensharingCaptureTakenEvent } from '../analytics/AnalyticsEvents';
|
||||
@@ -7,20 +7,20 @@ import { sendAnalytics } from '../analytics/functions';
|
||||
import { IReduxState } from '../app/types';
|
||||
import { getCurrentConference } from '../base/conference/functions';
|
||||
import { getLocalParticipant, getRemoteParticipants } from '../base/participants/functions';
|
||||
import { ITrack } from '../base/tracks/types';
|
||||
import { getBaseUrl } from '../base/util/helpers';
|
||||
import { extractFqnFromPath } from '../dynamic-branding/functions.any';
|
||||
|
||||
import {
|
||||
CLEAR_INTERVAL,
|
||||
INTERVAL_TIMEOUT,
|
||||
PERCENTAGE_LOWER_BOUND,
|
||||
CLEAR_TIMEOUT,
|
||||
POLL_INTERVAL,
|
||||
SET_INTERVAL
|
||||
SCREENSHOT_QUEUE_LIMIT,
|
||||
SET_TIMEOUT,
|
||||
TIMEOUT_TICK
|
||||
} from './constants';
|
||||
import logger from './logger';
|
||||
// eslint-disable-next-line lines-around-comment
|
||||
// @ts-ignore
|
||||
import { processScreenshot } from './processScreenshot';
|
||||
import { timerWorkerScript } from './worker';
|
||||
|
||||
declare let ImageCapture: any;
|
||||
|
||||
@@ -30,14 +30,10 @@ declare let ImageCapture: any;
|
||||
*/
|
||||
export default class ScreenshotCaptureSummary {
|
||||
_state: IReduxState;
|
||||
_currentCanvas: HTMLCanvasElement;
|
||||
_currentCanvasContext: CanvasRenderingContext2D | null;
|
||||
_initializedRegion: boolean;
|
||||
_imageCapture: any;
|
||||
_imageCapture: ImageCapture;
|
||||
_streamWorker: Worker;
|
||||
_streamHeight: any;
|
||||
_streamWidth: any;
|
||||
_storedImageData?: ImageData;
|
||||
_queue: Blob[];
|
||||
|
||||
/**
|
||||
* Initializes a new {@code ScreenshotCaptureEffect} instance.
|
||||
@@ -46,16 +42,17 @@ export default class ScreenshotCaptureSummary {
|
||||
*/
|
||||
constructor(state: IReduxState) {
|
||||
this._state = state;
|
||||
this._currentCanvas = document.createElement('canvas');
|
||||
this._currentCanvasContext = this._currentCanvas.getContext('2d');
|
||||
|
||||
// Bind handlers such that they access the same instance.
|
||||
this._handleWorkerAction = this._handleWorkerAction.bind(this);
|
||||
this._initScreenshotCapture = this._initScreenshotCapture.bind(this);
|
||||
this._streamWorker = new Worker(timerWorkerScript, { name: 'Screenshot capture worker' });
|
||||
const baseUrl = `${getBaseUrl()}libs/`;
|
||||
const workerUrl = `${baseUrl}screenshot-capture-worker.min.js`;
|
||||
|
||||
this._streamWorker = new Worker(workerUrl, { name: 'Screenshot capture worker' });
|
||||
this._streamWorker.onmessage = this._handleWorkerAction;
|
||||
|
||||
this._initializedRegion = false;
|
||||
this._queue = [];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -77,10 +74,17 @@ export default class ScreenshotCaptureSummary {
|
||||
...jwt && { 'Authorization': `Bearer ${jwt}` }
|
||||
};
|
||||
|
||||
await fetch(`${_screenshotHistoryRegionUrl}/${sessionId}`, {
|
||||
method: 'POST',
|
||||
headers
|
||||
});
|
||||
try {
|
||||
await fetch(`${_screenshotHistoryRegionUrl}/${sessionId}`, {
|
||||
method: 'POST',
|
||||
headers
|
||||
});
|
||||
} catch (err) {
|
||||
logger.warn(`Could not create screenshot region: ${err}`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
this._initializedRegion = true;
|
||||
}
|
||||
@@ -88,31 +92,27 @@ export default class ScreenshotCaptureSummary {
|
||||
/**
|
||||
* Starts the screenshot capture event on a loop.
|
||||
*
|
||||
* @param {Track} track - The track that contains the stream from which screenshots are to be sent.
|
||||
* @param {JitsiTrack} jitsiTrack - The track that contains the stream from which screenshots are to be sent.
|
||||
* @returns {Promise} - Promise that resolves once effect has started or rejects if the
|
||||
* videoType parameter is not desktop.
|
||||
*/
|
||||
async start(track: ITrack) {
|
||||
const { videoType } = track;
|
||||
const stream = track.getOriginalStream();
|
||||
async start(jitsiTrack: JitsiTrack) {
|
||||
if (!window.OffscreenCanvas) {
|
||||
logger.warn('Can\'t start screenshot capture, OffscreenCanvas is not available');
|
||||
|
||||
return;
|
||||
}
|
||||
const { videoType, track } = jitsiTrack;
|
||||
|
||||
if (videoType !== 'desktop') {
|
||||
return;
|
||||
}
|
||||
const desktopTrack = stream.getVideoTracks()[0];
|
||||
const { height, width }
|
||||
= desktopTrack.getSettings() ?? desktopTrack.getConstraints();
|
||||
|
||||
this._streamHeight = height;
|
||||
this._streamWidth = width;
|
||||
this._currentCanvas.height = parseInt(height, 10);
|
||||
this._currentCanvas.width = parseInt(width, 10);
|
||||
this._imageCapture = new ImageCapture(desktopTrack);
|
||||
this._imageCapture = new ImageCapture(track);
|
||||
|
||||
if (!this._initializedRegion) {
|
||||
await this._initRegionSelection();
|
||||
}
|
||||
this._initScreenshotCapture();
|
||||
this.sendTimeout();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -121,28 +121,34 @@ export default class ScreenshotCaptureSummary {
|
||||
* @returns {void}
|
||||
*/
|
||||
stop() {
|
||||
this._streamWorker.postMessage({ id: CLEAR_INTERVAL });
|
||||
this._streamWorker.postMessage({ id: CLEAR_TIMEOUT });
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that is called as soon as the first frame of the video loads from stream.
|
||||
* The method is used to store the {@code ImageData} object from the first frames
|
||||
* in order to use it for future comparisons based on which we can process only certain
|
||||
* screenshots.
|
||||
* Sends to worker the imageBitmap for the next timeout.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async _initScreenshotCapture() {
|
||||
const imageBitmap = await this._imageCapture.grabFrame();
|
||||
async sendTimeout() {
|
||||
let imageBitmap: ImageBitmap | undefined;
|
||||
|
||||
this._currentCanvasContext?.drawImage(imageBitmap, 0, 0, this._streamWidth, this._streamHeight);
|
||||
const imageData = this._currentCanvasContext?.getImageData(0, 0, this._streamWidth, this._streamHeight);
|
||||
if (!this._imageCapture.track || this._imageCapture.track.readyState !== 'live') {
|
||||
logger.warn('Track is in invalid state');
|
||||
this.stop();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
imageBitmap = await this._imageCapture.grabFrame();
|
||||
} catch (e) {
|
||||
// ignore error
|
||||
}
|
||||
|
||||
this._storedImageData = imageData;
|
||||
this._streamWorker.postMessage({
|
||||
id: SET_INTERVAL,
|
||||
timeMs: POLL_INTERVAL
|
||||
id: SET_TIMEOUT,
|
||||
timeMs: POLL_INTERVAL,
|
||||
imageBitmap
|
||||
});
|
||||
}
|
||||
|
||||
@@ -153,18 +159,24 @@ export default class ScreenshotCaptureSummary {
|
||||
* @param {EventHandler} message - Message received from the Worker.
|
||||
* @returns {void}
|
||||
*/
|
||||
_handleWorkerAction(message: { data: { id: number; }; }) {
|
||||
return message.data.id === INTERVAL_TIMEOUT && this._handleScreenshot();
|
||||
_handleWorkerAction(message: { data: { id: number; imageBlob?: Blob; }; }) {
|
||||
const { id, imageBlob } = message.data;
|
||||
|
||||
this.sendTimeout();
|
||||
if (id === TIMEOUT_TICK && imageBlob && this._queue.length < SCREENSHOT_QUEUE_LIMIT) {
|
||||
this._doProcessScreenshot(imageBlob);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that processes the screenshot.
|
||||
*
|
||||
* @private
|
||||
* @param {ImageData} imageData - The image data of the new screenshot.
|
||||
* @param {Blob} imageBlob - The blob for the current screenshot.
|
||||
* @returns {void}
|
||||
*/
|
||||
_doProcessScreenshot(imageData?: ImageData) {
|
||||
_doProcessScreenshot(imageBlob: Blob) {
|
||||
this._queue.push(imageBlob);
|
||||
sendAnalytics(createScreensharingCaptureTakenEvent());
|
||||
|
||||
const conference = getCurrentConference(this._state);
|
||||
@@ -175,41 +187,24 @@ export default class ScreenshotCaptureSummary {
|
||||
const { jwt } = this._state['features/base/jwt'];
|
||||
const meetingFqn = extractFqnFromPath();
|
||||
const remoteParticipants = getRemoteParticipants(this._state);
|
||||
const participants = [];
|
||||
const participants: Array<string | undefined> = [];
|
||||
|
||||
participants.push(getLocalParticipant(this._state)?.id);
|
||||
remoteParticipants.forEach(p => participants.push(p.id));
|
||||
this._storedImageData = imageData;
|
||||
|
||||
processScreenshot(this._currentCanvas, {
|
||||
processScreenshot(imageBlob, {
|
||||
jid,
|
||||
jwt,
|
||||
sessionId,
|
||||
timestamp,
|
||||
meetingFqn,
|
||||
participants
|
||||
}).then(() => {
|
||||
const index = this._queue.indexOf(imageBlob);
|
||||
|
||||
if (index > -1) {
|
||||
this._queue.splice(index, 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Screenshot handler.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
async _handleScreenshot() {
|
||||
const imageBitmap = await this._imageCapture.grabFrame();
|
||||
|
||||
this._currentCanvasContext?.drawImage(imageBitmap, 0, 0, this._streamWidth, this._streamHeight);
|
||||
const imageData = this._currentCanvasContext?.getImageData(0, 0, this._streamWidth, this._streamHeight);
|
||||
|
||||
resemble(imageData ?? '')
|
||||
.compareTo(this._storedImageData ?? '')
|
||||
.setReturnEarlyThreshold(PERCENTAGE_LOWER_BOUND)
|
||||
.onComplete(resultData => {
|
||||
if (resultData.rawMisMatchPercentage > PERCENTAGE_LOWER_BOUND) {
|
||||
this._doProcessScreenshot(imageData);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,40 +1,44 @@
|
||||
/**
|
||||
* Percent of pixels that signal if two images should be considered different.
|
||||
*/
|
||||
export const PERCENTAGE_LOWER_BOUND = 5;
|
||||
export const PERCENTAGE_LOWER_BOUND = 4;
|
||||
|
||||
/**
|
||||
* Number of milliseconds that represent how often screenshots should be taken.
|
||||
*/
|
||||
export const POLL_INTERVAL = 4000;
|
||||
export const POLL_INTERVAL = 2000;
|
||||
|
||||
/**
|
||||
* SET_INTERVAL constant is used to set interval and it is set in
|
||||
* SET_TIMEOUT constant is used to set interval and it is set in
|
||||
* the id property of the request.data property. TimeMs property must
|
||||
* also be set. Request.data example:
|
||||
*
|
||||
* {
|
||||
* id: SET_INTERVAL,
|
||||
* id: SET_TIMEOUT,
|
||||
* timeMs: 33
|
||||
* }.
|
||||
*/
|
||||
export const SET_INTERVAL = 1;
|
||||
export const SET_TIMEOUT = 1;
|
||||
|
||||
/**
|
||||
* CLEAR_INTERVAL constant is used to clear the interval and it is set in
|
||||
* CLEAR_TIMEOUT constant is used to clear the interval and it is set in
|
||||
* the id property of the request.data property.
|
||||
*
|
||||
* {
|
||||
* id: CLEAR_INTERVAL
|
||||
* id: CLEAR_TIMEOUT
|
||||
* }.
|
||||
*/
|
||||
export const CLEAR_INTERVAL = 2;
|
||||
export const CLEAR_TIMEOUT = 2;
|
||||
|
||||
/**
|
||||
* INTERVAL_TIMEOUT constant is used as response and it is set in the id property.
|
||||
* TIMEOUT_TICK constant is used as response and it is set in the id property.
|
||||
*
|
||||
* {
|
||||
* id: INTERVAL_TIMEOUT
|
||||
* id: TIMEOUT_TICK
|
||||
* }.
|
||||
*/
|
||||
export const INTERVAL_TIMEOUT = 3;
|
||||
export const TIMEOUT_TICK = 3;
|
||||
|
||||
export const SCREENSHOT_QUEUE_LIMIT = 3;
|
||||
|
||||
export const MAX_FILE_SIZE = 1000000;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/**
|
||||
* Helper method used to process screenshots captured by the {@code ScreenshotCaptureEffect}.
|
||||
*
|
||||
* @param {HTMLCanvasElement} canvas - The canvas containing a screenshot to be processed.
|
||||
* @param {Blob} imageBlob - The blob of the screenshot that has to be processed.
|
||||
* @param {Object} options - Custom options required for processing.
|
||||
* @returns {void}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export function processScreenshot(canvas, options) { // eslint-disable-line no-unused-vars
|
||||
export async function processScreenshot(imageBlob, options) { // eslint-disable-line no-unused-vars
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,29 +1,132 @@
|
||||
import pixelmatch from 'pixelmatch';
|
||||
|
||||
import {
|
||||
CLEAR_INTERVAL,
|
||||
INTERVAL_TIMEOUT,
|
||||
SET_INTERVAL
|
||||
CLEAR_TIMEOUT,
|
||||
MAX_FILE_SIZE,
|
||||
PERCENTAGE_LOWER_BOUND,
|
||||
SET_TIMEOUT,
|
||||
TIMEOUT_TICK
|
||||
} from './constants';
|
||||
|
||||
const code = `
|
||||
var timer;
|
||||
|
||||
onmessage = function(request) {
|
||||
switch (request.data.id) {
|
||||
case ${SET_INTERVAL}: {
|
||||
timer = setInterval(() => {
|
||||
postMessage({ id: ${INTERVAL_TIMEOUT} });
|
||||
}, request.data.timeMs);
|
||||
break;
|
||||
}
|
||||
case ${CLEAR_INTERVAL}: {
|
||||
if (timer) {
|
||||
clearInterval(timer);
|
||||
let timer: ReturnType<typeof setTimeout>;
|
||||
const canvas = new OffscreenCanvas(0, 0);
|
||||
const ctx = canvas.getContext('2d');
|
||||
let storedImageData: ImageData | undefined;
|
||||
|
||||
/**
|
||||
* Sends Blob with the screenshot to main thread.
|
||||
*
|
||||
* @param {ImageData} imageData - The image of the screenshot.
|
||||
* @returns {void}
|
||||
*/
|
||||
async function sendBlob(imageData: ImageData) {
|
||||
let imageBlob = await canvas.convertToBlob({ type: 'image/jpeg' });
|
||||
|
||||
if (imageBlob.size > MAX_FILE_SIZE) {
|
||||
const quality = Number((MAX_FILE_SIZE / imageBlob.size).toFixed(2)) * 0.92;
|
||||
|
||||
imageBlob = await canvas.convertToBlob({ type: 'image/jpeg',
|
||||
quality });
|
||||
}
|
||||
|
||||
storedImageData = imageData;
|
||||
|
||||
postMessage({
|
||||
id: TIMEOUT_TICK,
|
||||
imageBlob
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends empty message to main thread.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
function sendEmpty() {
|
||||
postMessage({
|
||||
id: TIMEOUT_TICK
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the image bitmap on the canvas and checks the difference percent with the previous image
|
||||
* if there is no previous image the percentage is not calculated.
|
||||
*
|
||||
* @param {ImageBitmap} imageBitmap - The image bitmap that is drawn on canvas.
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkScreenshot(imageBitmap: ImageBitmap) {
|
||||
const { height, width } = imageBitmap;
|
||||
|
||||
if (canvas.width !== width) {
|
||||
canvas.width = width;
|
||||
}
|
||||
|
||||
if (canvas.height !== height) {
|
||||
canvas.height = height;
|
||||
}
|
||||
|
||||
ctx?.drawImage(imageBitmap, 0, 0, width, height);
|
||||
const imageData = ctx?.getImageData(0, 0, width, height);
|
||||
|
||||
imageBitmap.close();
|
||||
|
||||
if (!imageData) {
|
||||
sendEmpty();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!storedImageData || imageData.data.length !== storedImageData.data.length) {
|
||||
sendBlob(imageData);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let numOfPixels = 0;
|
||||
|
||||
try {
|
||||
numOfPixels = pixelmatch(
|
||||
imageData.data,
|
||||
storedImageData.data,
|
||||
null,
|
||||
width,
|
||||
height);
|
||||
} catch {
|
||||
sendEmpty();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const percent = numOfPixels / imageData.data.length * 100;
|
||||
|
||||
if (percent >= PERCENTAGE_LOWER_BOUND) {
|
||||
sendBlob(imageData);
|
||||
} else {
|
||||
sendEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
onmessage = function(request) {
|
||||
switch (request.data.id) {
|
||||
case SET_TIMEOUT: {
|
||||
timer = setTimeout(async () => {
|
||||
const imageBitmap = request.data.imageBitmap;
|
||||
|
||||
if (imageBitmap) {
|
||||
checkScreenshot(imageBitmap);
|
||||
} else {
|
||||
sendEmpty();
|
||||
}
|
||||
break;
|
||||
}, request.data.timeMs);
|
||||
break;
|
||||
}
|
||||
case CLEAR_TIMEOUT: {
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
};
|
||||
`;
|
||||
|
||||
// @ts-ignore
|
||||
export const timerWorkerScript = URL.createObjectURL(new Blob([ code ], { type: 'application/javascript' }));
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -153,10 +153,15 @@ function _endpointMessageReceived({ dispatch, getState }: IStore, next: Function
|
||||
newTranscriptMessage.unstable = text;
|
||||
}
|
||||
|
||||
dispatch(
|
||||
updateTranscriptMessage(
|
||||
transcriptMessageID,
|
||||
newTranscriptMessage));
|
||||
dispatch(updateTranscriptMessage(transcriptMessageID, newTranscriptMessage));
|
||||
|
||||
// Notify the external API too.
|
||||
if (typeof APP !== 'undefined') {
|
||||
APP.API.notifyTranscriptionChunkReceived({
|
||||
messageID: transcriptMessageID,
|
||||
...newTranscriptMessage
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error occurred while updating transcriptions\n', error);
|
||||
|
||||
@@ -73,7 +73,7 @@ const useStyles = makeStyles<{ overflowDrawer: boolean; reactionsMenuHeight: num
|
||||
return {
|
||||
overflowMenuDrawer: {
|
||||
overflow: 'hidden',
|
||||
height: `calc(${DRAWER_MAX_HEIGHT} - ${reactionsMenuHeight}px - 16px)`
|
||||
height: `calc(${DRAWER_MAX_HEIGHT})`
|
||||
},
|
||||
contextMenu: {
|
||||
position: 'relative' as const,
|
||||
@@ -238,6 +238,7 @@ const OverflowMenuButton = ({
|
||||
trigger = 'click'
|
||||
visible = { isOpen }>
|
||||
<OverflowToggleButton
|
||||
isMenuButton = { true }
|
||||
isOpen = { isOpen }
|
||||
onKeyDown = { onEscClick } />
|
||||
</Popover>
|
||||
|
||||
@@ -35,6 +35,7 @@ class WelcomePage extends AbstractWelcomePage<IProps> {
|
||||
_additionalCardTemplate: HTMLTemplateElement | null;
|
||||
_additionalContentTemplate: HTMLTemplateElement | null;
|
||||
_additionalToolbarContentTemplate: HTMLTemplateElement | null;
|
||||
_titleHasNotAllowCharacter: boolean;
|
||||
|
||||
/**
|
||||
* Default values for {@code WelcomePage} component's properties.
|
||||
@@ -61,6 +62,14 @@ class WelcomePage extends AbstractWelcomePage<IProps> {
|
||||
interfaceConfig.GENERATE_ROOMNAMES_ON_WELCOME_PAGE
|
||||
};
|
||||
|
||||
/**
|
||||
* Used To display a warning massage if the title input has no allow character.
|
||||
*
|
||||
* @private
|
||||
* @type {boolean}
|
||||
*/
|
||||
this._titleHasNotAllowCharacter = false;
|
||||
|
||||
/**
|
||||
* The HTML Element used as the container for additional content. Used
|
||||
* for directly appending the additional content template to the dom.
|
||||
@@ -205,7 +214,7 @@ class WelcomePage extends AbstractWelcomePage<IProps> {
|
||||
<SettingsButton
|
||||
defaultTab = { SETTINGS_TABS.CALENDAR }
|
||||
isDisplayedOnWelcomePage = { true } />
|
||||
{ showAdditionalToolbarContent
|
||||
{showAdditionalToolbarContent
|
||||
? <div
|
||||
className = 'settings-toolbar-content'
|
||||
ref = { this._setAdditionalToolbarContentRef } />
|
||||
@@ -213,10 +222,10 @@ class WelcomePage extends AbstractWelcomePage<IProps> {
|
||||
}
|
||||
</div>
|
||||
<h1 className = 'header-text-title'>
|
||||
{ t('welcomepage.headerTitle') }
|
||||
{t('welcomepage.headerTitle')}
|
||||
</h1>
|
||||
<span className = 'header-text-subtitle'>
|
||||
{ t('welcomepage.headerSubtitle')}
|
||||
{t('welcomepage.headerSubtitle')}
|
||||
</span>
|
||||
<div id = 'enter_room'>
|
||||
<div className = 'join-meeting-container'>
|
||||
@@ -232,11 +241,11 @@ class WelcomePage extends AbstractWelcomePage<IProps> {
|
||||
pattern = { ROOM_NAME_VALIDATE_PATTERN_STR }
|
||||
placeholder = { this.state.roomPlaceholder }
|
||||
ref = { this._setRoomInputRef }
|
||||
title = { t('welcomepage.roomNameAllowedChars') }
|
||||
type = 'text'
|
||||
value = { this.state.room } />
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<button
|
||||
aria-disabled = 'false'
|
||||
aria-label = 'Start meeting'
|
||||
@@ -245,17 +254,27 @@ class WelcomePage extends AbstractWelcomePage<IProps> {
|
||||
onClick = { this._onFormSubmit }
|
||||
tabIndex = { 0 }
|
||||
type = 'button'>
|
||||
{ t('welcomepage.startMeeting') }
|
||||
{t('welcomepage.startMeeting')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{ this._renderInsecureRoomNameWarning() }
|
||||
{this._titleHasNotAllowCharacter && (
|
||||
<div
|
||||
className = 'not-allow-title-character-div'
|
||||
role = 'alert'>
|
||||
<Icon src = { IconWarning } />
|
||||
<span className = 'not-allow-title-character-text'>
|
||||
{t('welcomepage.roomNameAllowedChars')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{this._renderInsecureRoomNameWarning()}
|
||||
|
||||
{ _moderatedRoomServiceUrl && (
|
||||
{_moderatedRoomServiceUrl && (
|
||||
<div id = 'moderated-meetings'>
|
||||
{
|
||||
translateToHTML(
|
||||
t, 'welcomepage.moderatedMessage', { url: _moderatedRoomServiceUrl })
|
||||
t, 'welcomepage.moderatedMessage', { url: _moderatedRoomServiceUrl })
|
||||
}
|
||||
</div>)}
|
||||
</div>
|
||||
@@ -264,22 +283,22 @@ class WelcomePage extends AbstractWelcomePage<IProps> {
|
||||
<div className = 'welcome-cards-container'>
|
||||
<div className = 'welcome-card-column'>
|
||||
<div className = 'welcome-tabs welcome-card welcome-card--blue'>
|
||||
{ this._renderTabs() }
|
||||
{this._renderTabs()}
|
||||
</div>
|
||||
{ showAdditionalCard
|
||||
{showAdditionalCard
|
||||
? <div
|
||||
className = 'welcome-card welcome-card--dark'
|
||||
ref = { this._setAdditionalCardRef } />
|
||||
: null }
|
||||
: null}
|
||||
</div>
|
||||
|
||||
{ showAdditionalContent
|
||||
{showAdditionalContent
|
||||
? <div
|
||||
className = 'welcome-page-content'
|
||||
ref = { this._setAdditionalContentRef } />
|
||||
: null }
|
||||
: null}
|
||||
</div>
|
||||
{ DISPLAY_WELCOME_FOOTER && this._renderFooter()}
|
||||
{DISPLAY_WELCOME_FOOTER && this._renderFooter()}
|
||||
</div>
|
||||
|
||||
);
|
||||
@@ -295,7 +314,7 @@ class WelcomePage extends AbstractWelcomePage<IProps> {
|
||||
<div className = 'insecure-room-name-warning'>
|
||||
<Icon src = { IconWarning } />
|
||||
<span>
|
||||
{ getUnsafeRoomText(this.props.t, 'welcome') }
|
||||
{getUnsafeRoomText(this.props.t, 'welcome')}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
@@ -329,6 +348,9 @@ class WelcomePage extends AbstractWelcomePage<IProps> {
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line require-jsdoc
|
||||
_onRoomChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
const specialCharacters = [ '?', '&', ':', '\'', '"', '%', '#', '.' ];
|
||||
|
||||
this._titleHasNotAllowCharacter = specialCharacters.some(char => event.target.value.includes(char));
|
||||
super._onRoomChange(event.target.value);
|
||||
}
|
||||
|
||||
@@ -342,8 +364,10 @@ class WelcomePage extends AbstractWelcomePage<IProps> {
|
||||
t,
|
||||
_deeplinkingCfg: {
|
||||
ios = { downloadLink: undefined },
|
||||
android = { fDroidUrl: undefined,
|
||||
downloadLink: undefined }
|
||||
android = {
|
||||
fDroidUrl: undefined,
|
||||
downloadLink: undefined
|
||||
}
|
||||
}
|
||||
} = this.props;
|
||||
|
||||
|
||||
@@ -292,10 +292,12 @@ end);
|
||||
module:hook('muc-occupant-groupchat', function(event)
|
||||
local occupant, room, stanza = event.occupant, event.room, event.stanza;
|
||||
local from = stanza.attr.from;
|
||||
local occupant_host = jid.host(occupant.bare_jid);
|
||||
local occupant_host;
|
||||
|
||||
-- if there is no occupant this is a message from main, probably coming from other vnode
|
||||
if occupant then
|
||||
occupant_host = jid.host(occupant.bare_jid);
|
||||
|
||||
-- we manage nick only for visitors
|
||||
if occupant_host ~= main_domain then
|
||||
-- add to message stanza display name for the visitor
|
||||
|
||||
@@ -50,9 +50,7 @@ module:hook("muc-occupant-pre-join", function (event)
|
||||
return;
|
||||
end
|
||||
|
||||
-- FIX ME: luacheck warning 581
|
||||
-- not (x == y)' can be replaced by 'x ~= y' (if neither side is a table or NaN)
|
||||
if not (subdomain == session.jitsi_meet_domain) then
|
||||
if session.jitsi_meet_domain ~= '*' and subdomain ~= session.jitsi_meet_domain then
|
||||
module:log('debug', 'skip allowners for auth user and non matching room subdomain: %s, jwt subdomain: %s',
|
||||
subdomain, session.jitsi_meet_domain);
|
||||
return;
|
||||
@@ -138,7 +136,7 @@ function filter_admin_set_query(event)
|
||||
local _aff = item.attr.affiliation;
|
||||
|
||||
-- if it is a moderated room we skip it
|
||||
if is_moderated(room.jid) then
|
||||
if room and is_moderated(room.jid) then
|
||||
return nil;
|
||||
end
|
||||
|
||||
|
||||
@@ -342,12 +342,11 @@ end
|
||||
function on_breakout_room_pre_create(event)
|
||||
local breakout_room = event.room;
|
||||
local main_room, main_room_jid = get_main_room(breakout_room.jid);
|
||||
local name = main_room._data.breakout_rooms[breakout_room.jid];
|
||||
|
||||
-- Only allow existent breakout rooms to be started.
|
||||
-- Authorisation of breakout rooms is done by their random uuid name
|
||||
if main_room and main_room._data.breakout_rooms and name then
|
||||
breakout_room:set_subject(breakout_room.jid, name);
|
||||
if main_room and main_room._data.breakout_rooms and main_room._data.breakout_rooms[breakout_room.jid] then
|
||||
breakout_room:set_subject(breakout_room.jid, main_room._data.breakout_rooms[breakout_room.jid]);
|
||||
else
|
||||
module:log('debug', 'Invalid breakout room %s will not be created.', breakout_room.jid);
|
||||
breakout_room:destroy(main_room_jid, 'Breakout room is invalid.');
|
||||
|
||||
@@ -62,7 +62,7 @@ module:hook("muc-occupant-pre-join", function(event)
|
||||
module:log("debug", "Bypass lobby invitee %s", occupant_jid)
|
||||
occupant.role = "participant";
|
||||
room:set_affiliation(true, jid_bare(occupant_jid), "member")
|
||||
room:save();
|
||||
room:save_occupant(occupant);
|
||||
end
|
||||
-- bypass password on the flip device
|
||||
local join = stanza:get_child("x", MUC_NS);
|
||||
@@ -111,12 +111,19 @@ end)
|
||||
module:hook("muc-occupant-joined", function(event)
|
||||
local room, occupant = event.room, event.occupant;
|
||||
if is_healthcheck_room(room.jid) or is_admin(occupant.bare_jid) then
|
||||
return ;
|
||||
return;
|
||||
end
|
||||
|
||||
if room._data.flip_participant_nick and occupant.nick == room._data.flip_participant_nick then
|
||||
-- make joining participant from flip device have the same role and affiliation as for the previous device
|
||||
local kicked_occupant = room:get_occupant_by_nick(room._data.kicked_participant_nick);
|
||||
|
||||
if not kicked_occupant then
|
||||
module:log("info", "Kick participant not found, nick %s from main room jid %s",
|
||||
room._data.kicked_participant_nick, room.jid)
|
||||
return;
|
||||
end
|
||||
|
||||
local initial_affiliation = room:get_affiliation(kicked_occupant.jid) or "member";
|
||||
module:log("debug", "Transfer affiliation %s to occupant jid %s", initial_affiliation, occupant.jid)
|
||||
room:set_affiliation(true, occupant.bare_jid, initial_affiliation)
|
||||
@@ -129,7 +136,7 @@ module:hook("muc-occupant-joined", function(event)
|
||||
local kicked_participant_node_jid = jid.split(kicked_occupant.jid);
|
||||
module:log("info", "Kick participant jid %s nick %s from main room jid %s", kicked_occupant.jid, room._data.kicked_participant_nick, room.jid)
|
||||
room:set_role(true, room._data.kicked_participant_nick, 'none')
|
||||
room:save()
|
||||
room:save_occupant(occupant);
|
||||
-- Kick participant from the first device from the lobby room
|
||||
if room._data.lobbyroom then
|
||||
local lobby_room_jid = room._data.lobbyroom;
|
||||
|
||||
@@ -165,6 +165,12 @@ function filter_stanza(stanza)
|
||||
-- allow messages to or from moderator
|
||||
local lobby_room_jid = jid_bare(stanza.attr.from);
|
||||
local lobby_room = lobby_muc_service.get_room_from_jid(lobby_room_jid);
|
||||
|
||||
if not lobby_room then
|
||||
module:log('warn', 'No lobby room found %s', stanza.attr.from);
|
||||
return nil;
|
||||
end
|
||||
|
||||
local is_to_moderator = lobby_room:get_affiliation(stanza.attr.to) == 'owner';
|
||||
local from_occupant = lobby_room:get_occupant_by_nick(stanza.attr.from);
|
||||
|
||||
@@ -396,7 +402,7 @@ process_host_module(main_muc_component_config, function(host_module, host)
|
||||
if not affiliation or affiliation == 'none' or affiliation == 'member' then
|
||||
occupant.role = 'participant';
|
||||
room:set_affiliation(true, invitee_bare_jid, 'member');
|
||||
room:save();
|
||||
room:save_occupant(occupant);
|
||||
|
||||
return;
|
||||
end
|
||||
|
||||
@@ -3,13 +3,15 @@
|
||||
local queue = require "util.queue";
|
||||
local new_throttle = require "util.throttle".create;
|
||||
local timer = require "util.timer";
|
||||
local st = require "util.stanza";
|
||||
|
||||
-- we max to 500 participants per meeting so this should be enough, we are not suppose to handle all
|
||||
-- participants in one meeting
|
||||
local PRESENCE_QUEUE_MAX_SIZE = 1000;
|
||||
|
||||
-- default to 5 participants per second
|
||||
local join_rate_per_conference = module:get_option_number("muc_rate_joins", 5);
|
||||
-- default to 3 participants per second
|
||||
local join_rate_per_conference = module:get_option_number("muc_rate_joins", 3);
|
||||
local leave_rate_per_conference = module:get_option_number("muc_rate_leaves", 5);
|
||||
|
||||
-- Measure/monitor the room rate limiting queue
|
||||
local measure = require "core.statsmanager".measure;
|
||||
@@ -31,16 +33,20 @@ local stat_longest_queue = 0;
|
||||
|
||||
-- Adds item to the queue
|
||||
-- @returns false if queue is full and item was not added, true otherwise
|
||||
local function add_item_to_queue(joining_queue, item, room, from)
|
||||
if not joining_queue:push(item) then
|
||||
module:log('error', 'Error pushing presence in queue for %s in %s', from, room.jid);
|
||||
local function add_item_to_queue(queue, item, room, from, send_stats)
|
||||
if not queue:push(item) then
|
||||
module:log('error',
|
||||
'Error pushing item in %s queue for %s in %s', send_stats and 'join' or 'leave', from, room.jid);
|
||||
|
||||
if send_stats then
|
||||
measure_full_queue();
|
||||
end
|
||||
|
||||
measure_full_queue();
|
||||
return false;
|
||||
else
|
||||
-- check is this the longest queue and if so throws a stat
|
||||
if joining_queue:count() > stat_longest_queue then
|
||||
stat_longest_queue = joining_queue:count();
|
||||
if send_stats and queue:count() > stat_longest_queue then
|
||||
stat_longest_queue = queue:count();
|
||||
measure_longest_queue(stat_longest_queue);
|
||||
end
|
||||
|
||||
@@ -50,27 +56,23 @@ end
|
||||
|
||||
-- process join_rate_presence_queue in the room and pops element passing them to handle_normal_presence
|
||||
-- returns 1 if we want to reschedule it after 1 second
|
||||
local function timer_process_queue_elements (room)
|
||||
local presence_queue = room.join_rate_presence_queue;
|
||||
|
||||
if not presence_queue or presence_queue:count() == 0 then
|
||||
local function timer_process_queue_elements (rate, queue, process, queue_empty_cb)
|
||||
if not queue or queue:count() == 0 then
|
||||
return;
|
||||
end
|
||||
|
||||
for _ = 1, join_rate_per_conference do
|
||||
local ev = presence_queue:pop();
|
||||
for _ = 1, rate do
|
||||
local ev = queue:pop();
|
||||
if ev then
|
||||
-- we mark what we pass here so we can skip it on the next muc-occupant-pre-join event
|
||||
ev.stanza.delayed_join_skip = true;
|
||||
room:handle_normal_presence(ev.origin, ev.stanza);
|
||||
process(ev);
|
||||
end
|
||||
end
|
||||
|
||||
-- if there are elements left, schedule an execution in a second
|
||||
if presence_queue:count() > 0 then
|
||||
if queue:count() > 0 then
|
||||
return 1;
|
||||
else
|
||||
room.join_rate_queue_timer = false;
|
||||
queue_empty_cb();
|
||||
end
|
||||
end
|
||||
|
||||
@@ -99,17 +101,28 @@ module:hook("muc-occupant-pre-join", function (event)
|
||||
room.join_rate_presence_queue = queue.new(PRESENCE_QUEUE_MAX_SIZE);
|
||||
end
|
||||
|
||||
if not add_item_to_queue(room.join_rate_presence_queue, event, room, stanza.attr.from) then
|
||||
if not add_item_to_queue(room.join_rate_presence_queue, event, room, stanza.attr.from, true) then
|
||||
-- let's not stop processing the event
|
||||
return nil;
|
||||
end
|
||||
|
||||
if not room.join_rate_queue_timer then
|
||||
timer.add_task(1, function ()
|
||||
local status, result = pcall(timer_process_queue_elements, room);
|
||||
local status, result = pcall(timer_process_queue_elements,
|
||||
join_rate_per_conference,
|
||||
room.join_rate_presence_queue,
|
||||
function(ev)
|
||||
-- we mark what we pass here so we can skip it on the next muc-occupant-pre-join event
|
||||
ev.stanza.delayed_join_skip = true;
|
||||
room:handle_normal_presence(ev.origin, ev.stanza);
|
||||
end,
|
||||
function() -- empty callback
|
||||
room.join_rate_queue_timer = false;
|
||||
end
|
||||
);
|
||||
if not status then
|
||||
-- there was an error in the timer function
|
||||
module:log('error', 'Error processing queue: %s', result);
|
||||
module:log('error', 'Error processing join queue: %s', result);
|
||||
|
||||
measure_errors_processing_queue();
|
||||
|
||||
@@ -130,7 +143,7 @@ module:hook("muc-occupant-pre-join", function (event)
|
||||
|
||||
-- if add fails as queue is full we return false and the event will continue processing, we risk re-order
|
||||
-- but not losing it
|
||||
return add_item_to_queue(room.join_rate_presence_queue, event, room, stanza.attr.from);
|
||||
return add_item_to_queue(room.join_rate_presence_queue, event, room, stanza.attr.from, true);
|
||||
end
|
||||
|
||||
end, 9); -- as we will rate limit joins we need to be the first to execute
|
||||
@@ -141,3 +154,73 @@ end, 9); -- as we will rate limit joins we need to be the first to execute
|
||||
module:hook('muc-room-destroyed',function(event)
|
||||
event.room.join_rate_presence_queue = nil;
|
||||
end);
|
||||
|
||||
module:hook('muc-occupant-pre-leave', function (event)
|
||||
local occupant, room, stanza = event.occupant, event.room, event.stanza;
|
||||
local throttle = room.leave_rate_throttle;
|
||||
|
||||
if not throttle then
|
||||
throttle = new_throttle(leave_rate_per_conference, 1); -- rate per one second
|
||||
room.leave_rate_throttle = throttle;
|
||||
end
|
||||
|
||||
if not throttle:poll(1) then
|
||||
if not room.leave_rate_presence_queue then
|
||||
room.leave_rate_presence_queue = queue.new(PRESENCE_QUEUE_MAX_SIZE);
|
||||
end
|
||||
|
||||
-- we need it later when processing the event
|
||||
event.orig_role = occupant.role;
|
||||
|
||||
if not add_item_to_queue(room.leave_rate_presence_queue, event, room, stanza.attr.from, false) then
|
||||
-- let's not stop processing the event
|
||||
return nil;
|
||||
end
|
||||
|
||||
-- set role to nil so the occupant will be removed from room occupants when we save it
|
||||
-- we remove occupant from the list early on batches so we can spare sending few presences
|
||||
occupant.role = nil;
|
||||
room:save_occupant(occupant);
|
||||
|
||||
if not room.leave_rate_queue_timer then
|
||||
timer.add_task(1, function ()
|
||||
local status, result = pcall(timer_process_queue_elements,
|
||||
leave_rate_per_conference,
|
||||
room.leave_rate_presence_queue,
|
||||
function(ev)
|
||||
local occupant, orig_role, origin, room, stanza
|
||||
= ev.occupant, ev.orig_role, ev.origin, ev.room, ev.stanza;
|
||||
|
||||
room:publicise_occupant_status(
|
||||
occupant,
|
||||
st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user";}),
|
||||
nil, nil, nil, orig_role);
|
||||
|
||||
module:fire_event("muc-occupant-left", {
|
||||
room = room;
|
||||
nick = occupant.nick;
|
||||
occupant = occupant;
|
||||
origin = origin;
|
||||
stanza = stanza;
|
||||
});
|
||||
end,
|
||||
function() -- empty callback
|
||||
room.leave_rate_queue_timer = false;
|
||||
end
|
||||
);
|
||||
if not status then
|
||||
-- there was an error in the timer function
|
||||
module:log('error', 'Error processing leave queue: %s', result);
|
||||
|
||||
-- let's re-schedule timer so we do not lose the queue
|
||||
return 1;
|
||||
end
|
||||
|
||||
return result;
|
||||
end);
|
||||
room.leave_rate_queue_timer = true;
|
||||
end
|
||||
|
||||
return true; -- we stop execution, so we do not process this leave at the moment
|
||||
end
|
||||
end);
|
||||
|
||||
@@ -4,5 +4,7 @@
|
||||
local COMPONENT_IDENTITY_TYPE = 'room_metadata';
|
||||
local room_metadata_component_host = module:get_option_string('room_metadata_component', 'metadata.'..module.host);
|
||||
|
||||
module:depends("jitsi_session");
|
||||
|
||||
-- Advertise the component so clients can pick up the address and use it
|
||||
module:add_identity('component', COMPONENT_IDENTITY_TYPE, room_metadata_component_host);
|
||||
|
||||
@@ -35,15 +35,16 @@ end
|
||||
|
||||
-- Searches all rooms in the main muc component that holds a breakout room
|
||||
-- caches it if found so we don't search it again
|
||||
-- we should not cache objects in _data as this is being serialized when calling room:save()
|
||||
local function get_main_room(breakout_room)
|
||||
if breakout_room._data and breakout_room._data.main_room then
|
||||
return breakout_room._data.main_room;
|
||||
if breakout_room.main_room then
|
||||
return breakout_room.main_room;
|
||||
end
|
||||
|
||||
-- let's search all rooms to find the main room
|
||||
for room in main_muc_service.each_room() do
|
||||
if room._data and room._data.breakout_rooms_active and room._data.breakout_rooms[breakout_room.jid] then
|
||||
breakout_room._data.main_room = room;
|
||||
breakout_room.main_room = room;
|
||||
return room;
|
||||
end
|
||||
end
|
||||
@@ -117,7 +118,7 @@ function on_message(event)
|
||||
local from = event.stanza.attr.from;
|
||||
|
||||
local occupant = room:get_occupant_by_real_jid(from);
|
||||
if not occupant then
|
||||
if not occupant or not room.speakerStats[occupant.jid] then
|
||||
module:log("warn", "No occupant %s found for %s", from, roomAddress);
|
||||
return false;
|
||||
end
|
||||
|
||||
@@ -17,6 +17,7 @@ local starts_with = main_util.starts_with;
|
||||
local cjson_safe = require 'cjson.safe'
|
||||
local timer = require "util.timer";
|
||||
local async = require "util.async";
|
||||
local inspect = require 'inspect';
|
||||
|
||||
local nr_retries = 3;
|
||||
local ssl = require "ssl";
|
||||
@@ -197,6 +198,7 @@ end
|
||||
-- session.jitsi_meet_room - the room name value from the token
|
||||
-- session.jitsi_meet_domain - the domain name value from the token
|
||||
-- session.jitsi_meet_context_user - the user details from the token
|
||||
-- session.jitsi_meet_context_room - the room details from the token
|
||||
-- session.jitsi_meet_context_group - the group value from the token
|
||||
-- session.jitsi_meet_context_features - the features value from the token
|
||||
-- @param session the current session
|
||||
@@ -344,7 +346,11 @@ function Util:verify_room(session, room_address)
|
||||
|
||||
local auth_room = session.jitsi_meet_room;
|
||||
if auth_room then
|
||||
auth_room = string.lower(auth_room);
|
||||
if type(auth_room) == 'string' then
|
||||
auth_room = string.lower(auth_room);
|
||||
else
|
||||
module:log('warn', 'session.jitsi_meet_room not string: %s', inspect(auth_room));
|
||||
end
|
||||
end
|
||||
if not self.enableDomainVerification then
|
||||
-- if auth_room is missing, this means user is anonymous (no token for
|
||||
|
||||
@@ -383,6 +383,17 @@ module.exports = (_env, argv) => {
|
||||
|
||||
globalObject: 'AudioWorkletGlobalScope'
|
||||
}
|
||||
}),
|
||||
|
||||
Object.assign({}, config, {
|
||||
entry: {
|
||||
'screenshot-capture-worker': './react/features/screenshot-capture/worker.ts'
|
||||
},
|
||||
plugins: [
|
||||
...config.plugins,
|
||||
...getBundleAnalyzerPlugin(analyzeBundle, 'screenshot-capture-worker')
|
||||
],
|
||||
performance: getPerformanceHints(perfHintOptions, 4 * 1024)
|
||||
})
|
||||
];
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user