mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-02-04 04:50:20 +00:00
Compare commits
20 Commits
dependabot
...
release-82
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e2ccf5534 | ||
|
|
46c9c9f524 | ||
|
|
8cf16ca402 | ||
|
|
d3bc7e8399 | ||
|
|
1f7a4bcae3 | ||
|
|
65a8282931 | ||
|
|
67241d7847 | ||
|
|
17a6d23d3c | ||
|
|
a1ced791e2 | ||
|
|
edde9820c1 | ||
|
|
b70e4cfe56 | ||
|
|
b033c06ec2 | ||
|
|
abf06bc20e | ||
|
|
cd3f07b321 | ||
|
|
d950b70392 | ||
|
|
1b0dc0a8d7 | ||
|
|
4be62fb942 | ||
|
|
681a66508e | ||
|
|
b76264fc4b | ||
|
|
0fadf0437a |
@@ -136,6 +136,7 @@ import {
|
||||
isUserInteractionRequiredForUnmute
|
||||
} from './react/features/base/tracks/functions';
|
||||
import { downloadJSON } from './react/features/base/util/downloadJSON';
|
||||
import { getJitsiMeetGlobalNSConnectionTimes } from './react/features/base/util/helpers';
|
||||
import { openLeaveReasonDialog } from './react/features/conference/actions.web';
|
||||
import { showDesktopPicker } from './react/features/desktop-picker/actions';
|
||||
import { appendSuffix } from './react/features/display-name/functions';
|
||||
@@ -413,9 +414,10 @@ export default {
|
||||
* without any audio tracks.
|
||||
* @param {boolean} options.startWithVideoMuted - will start the conference
|
||||
* without any video tracks.
|
||||
* @param {boolean} recordTimeMetrics - If true time metrics will be recorded.
|
||||
* @returns {Promise<JitsiLocalTrack[]>, Object}
|
||||
*/
|
||||
createInitialLocalTracks(options = {}) {
|
||||
createInitialLocalTracks(options = {}, recordTimeMetrics = false) {
|
||||
const errors = {};
|
||||
|
||||
// Always get a handle on the audio input device so that we have statistics (such as "No audio input" or
|
||||
@@ -487,7 +489,7 @@ export default {
|
||||
devices: initialDevices,
|
||||
timeout,
|
||||
firePermissionPromptIsShownEvent: true
|
||||
})).then(({ tracks, errors: pErrors }) => {
|
||||
}, recordTimeMetrics)).then(({ tracks, errors: pErrors }) => {
|
||||
Object.assign(errors, pErrors);
|
||||
|
||||
return tracks;
|
||||
@@ -560,10 +562,10 @@ export default {
|
||||
* If prejoin page is enabled open an new connection in the background
|
||||
* and create local tracks.
|
||||
*
|
||||
* @param {{ roomName: string }} options
|
||||
* @param {{ roomName: string, shouldDispatchConnect }} options
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async init({ roomName }) {
|
||||
async init({ roomName, shouldDispatchConnect }) {
|
||||
const state = APP.store.getState();
|
||||
const initialOptions = {
|
||||
startAudioOnly: config.startAudioOnly,
|
||||
@@ -571,8 +573,12 @@ export default {
|
||||
startWithAudioMuted: getStartWithAudioMuted(state) || isUserInteractionRequiredForUnmute(state),
|
||||
startWithVideoMuted: getStartWithVideoMuted(state) || isUserInteractionRequiredForUnmute(state)
|
||||
};
|
||||
const connectionTimes = getJitsiMeetGlobalNSConnectionTimes();
|
||||
const startTime = window.performance.now();
|
||||
|
||||
logger.debug(`Executed conference.init with roomName: ${roomName}`);
|
||||
connectionTimes['conference.init.start'] = startTime;
|
||||
|
||||
logger.debug(`Executed conference.init with roomName: ${roomName} (performance.now=${startTime})`);
|
||||
|
||||
this.roomName = roomName;
|
||||
|
||||
@@ -605,29 +611,64 @@ export default {
|
||||
return localTracks;
|
||||
};
|
||||
const { dispatch, getState } = APP.store;
|
||||
const { tryCreateLocalTracks, errors } = this.createInitialLocalTracks(initialOptions);
|
||||
const createLocalTracksStart = window.performance.now();
|
||||
|
||||
dispatch(setInitialGUMPromise(tryCreateLocalTracks.then(async tr => {
|
||||
connectionTimes['conference.init.createLocalTracks.start'] = createLocalTracksStart;
|
||||
|
||||
logger.debug(`(TIME) createInitialLocalTracks: ${createLocalTracksStart} `);
|
||||
|
||||
const { tryCreateLocalTracks, errors } = this.createInitialLocalTracks(initialOptions, true);
|
||||
|
||||
tryCreateLocalTracks.then(async tr => {
|
||||
const createLocalTracksEnd = window.performance.now();
|
||||
|
||||
connectionTimes['conference.init.createLocalTracks.end'] = createLocalTracksEnd;
|
||||
logger.debug(`(TIME) createInitialLocalTracks finished: ${createLocalTracksEnd} `);
|
||||
const tracks = handleInitialTracks(initialOptions, tr);
|
||||
|
||||
this._initDeviceList(true);
|
||||
|
||||
const { initialGUMPromise } = getState()['features/base/media'];
|
||||
|
||||
if (isPrejoinPageVisible(getState())) {
|
||||
dispatch(gumPending([ MEDIA_TYPE.AUDIO, MEDIA_TYPE.VIDEO ], IGUMPendingState.NONE));
|
||||
dispatch(setInitialGUMPromise());
|
||||
|
||||
// Since the conference is not yet created in redux this function will execute synchronous
|
||||
// which will guarantee us that the local tracks are added to redux before we proceed.
|
||||
initPrejoin(tracks, errors, dispatch);
|
||||
|
||||
connectionTimes['conference.init.end'] = window.performance.now();
|
||||
|
||||
// resolve the initialGUMPromise in case connect have finished so that we can proceed to join.
|
||||
if (initialGUMPromise) {
|
||||
logger.debug('Resolving the initialGUM promise! (prejoinVisible=true)');
|
||||
initialGUMPromise.resolve({
|
||||
tracks,
|
||||
errors
|
||||
});
|
||||
}
|
||||
|
||||
logger.debug('Clear the initialGUM promise! (prejoinVisible=true)');
|
||||
|
||||
// For prejoin we don't need the initial GUM promise since the tracks are already added to the store
|
||||
// via initPrejoin
|
||||
dispatch(setInitialGUMPromise());
|
||||
} else {
|
||||
APP.store.dispatch(displayErrorsForCreateInitialLocalTracks(errors));
|
||||
setGUMPendingStateOnFailedTracks(tracks, APP.store.dispatch);
|
||||
|
||||
connectionTimes['conference.init.end'] = window.performance.now();
|
||||
if (initialGUMPromise) {
|
||||
logger.debug('Resolving the initialGUM promise!');
|
||||
initialGUMPromise.resolve({
|
||||
tracks,
|
||||
errors
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
tracks,
|
||||
errors
|
||||
};
|
||||
})));
|
||||
|
||||
if (!isPrejoinPageVisible(getState())) {
|
||||
if (shouldDispatchConnect) {
|
||||
logger.info('Dispatching connect from init since prejoin is not visible.');
|
||||
dispatch(connect());
|
||||
}
|
||||
@@ -2047,8 +2088,9 @@ export default {
|
||||
const { dispatch } = APP.store;
|
||||
|
||||
return dispatch(getAvailableDevices())
|
||||
.then(devices => {
|
||||
APP.UI.onAvailableDevicesChanged(devices);
|
||||
.then(() => {
|
||||
this.updateAudioIconEnabled();
|
||||
this.updateVideoIconEnabled();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2213,7 +2255,8 @@ export default {
|
||||
|
||||
return Promise.all(promises)
|
||||
.then(() => {
|
||||
APP.UI.onAvailableDevicesChanged(filteredDevices);
|
||||
this.updateAudioIconEnabled();
|
||||
this.updateVideoIconEnabled();
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -451,6 +451,10 @@ var config = {
|
||||
|
||||
// // Enables automatic turning on transcribing when recording is started
|
||||
// autoTranscribeOnRecord: false,
|
||||
|
||||
// // Enables automatic request of subtitles when transcriber is present in the meeting, uses the default
|
||||
// // language that is set
|
||||
// autoCaptionOnTranscribe: false,
|
||||
// },
|
||||
|
||||
// Misc
|
||||
@@ -758,6 +762,11 @@ var config = {
|
||||
// hideDisplayName: false,
|
||||
// // List of buttons to hide from the extra join options dropdown.
|
||||
// hideExtraJoinButtons: ['no-audio', 'by-phone'],
|
||||
// // Configuration for pre-call test
|
||||
// // By setting preCallTestEnabled, you enable the pre-call test in the prejoin page.
|
||||
// // ICE server credentials need to be provided over the preCallTestICEUrl
|
||||
// preCallTestEnabled: false,
|
||||
// preCallTestICEUrl: ''
|
||||
// },
|
||||
|
||||
// When 'true', the user cannot edit the display name.
|
||||
|
||||
@@ -739,6 +739,7 @@
|
||||
"connectedOneMember": "{{name}} joined the meeting",
|
||||
"connectedThreePlusMembers": "{{name}} and many others joined the meeting",
|
||||
"connectedTwoMembers": "{{first}} and {{second}} joined the meeting",
|
||||
"connectionFailed": "Connection failed. Please try again later!",
|
||||
"dataChannelClosed": "Video quality may be impaired",
|
||||
"dataChannelClosedDescription": "The bridge channel is down and thus video quality may be limited to its lowest setting.",
|
||||
"dataChannelClosedDescriptionWithAudio": "The bridge channel is down and thus disruptions to audio and video may occur.",
|
||||
@@ -753,6 +754,9 @@
|
||||
"gifsMenu": "GIPHY",
|
||||
"groupTitle": "Notifications",
|
||||
"hostAskedUnmute": "The moderator would like you to speak",
|
||||
"invalidTenant": "Invalid tenant",
|
||||
"invalidTenantHyphenDescription": "The tenant you are using is invalid (starts or ends with '-').",
|
||||
"invalidTenantLengthDescription": "The tenant you are using is too long.",
|
||||
"invitedOneMember": "{{name}} has been invited",
|
||||
"invitedThreePlusMembers": "{{name}} and {{count}} others have been invited",
|
||||
"invitedTwoMembers": "{{first}} and {{second}} have been invited",
|
||||
@@ -922,9 +926,11 @@
|
||||
"configuringDevices": "Configuring devices...",
|
||||
"connectedWithAudioQ": "You’re connected with audio?",
|
||||
"connection": {
|
||||
"failed": "Connection test failed!",
|
||||
"good": "Your internet connection looks good!",
|
||||
"nonOptimal": "Your internet connection is not optimal",
|
||||
"poor": "You have a poor internet connection"
|
||||
"poor": "You have a poor internet connection",
|
||||
"running": "Running connection test..."
|
||||
},
|
||||
"connectionDetails": {
|
||||
"audioClipping": "We expect your audio to be clipped.",
|
||||
@@ -933,6 +939,7 @@
|
||||
"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.",
|
||||
"testFailed": "The connection test encountered unexpected issues, but this might not impact your experience.",
|
||||
"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.",
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
requestEnableVideoModeration
|
||||
} from '../../react/features/av-moderation/actions';
|
||||
import { isEnabledFromState } from '../../react/features/av-moderation/functions';
|
||||
import { setAudioOnly } from '../../react/features/base/audio-only/actions';
|
||||
import {
|
||||
endConference,
|
||||
sendTones,
|
||||
@@ -565,6 +566,10 @@ function initCommands() {
|
||||
sendAnalytics(createApiEvent('set.video.quality'));
|
||||
APP.store.dispatch(setVideoQuality(frameHeight));
|
||||
},
|
||||
'set-audio-only': enable => {
|
||||
sendAnalytics(createApiEvent('set.audio.only'));
|
||||
APP.store.dispatch(setAudioOnly(enable));
|
||||
},
|
||||
'start-share-video': url => {
|
||||
sendAnalytics(createApiEvent('share.video.start'));
|
||||
const id = extractYoutubeIdOrURL(url);
|
||||
@@ -2218,6 +2223,19 @@ class API {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify the external application (if API is enabled) when the audio only enabled status changed.
|
||||
*
|
||||
* @param {boolean} enabled - Whether the audio only is enabled or not.
|
||||
* @returns {void}
|
||||
*/
|
||||
notifyAudioOnlyChanged(enabled) {
|
||||
this._sendEvent({
|
||||
name: 'audio-only-changed',
|
||||
enabled
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Disposes the allocated resources.
|
||||
*
|
||||
|
||||
2
modules/API/external/external_api.js
vendored
2
modules/API/external/external_api.js
vendored
@@ -59,6 +59,7 @@ const commands = {
|
||||
sendEndpointTextMessage: 'send-endpoint-text-message',
|
||||
sendParticipantToRoom: 'send-participant-to-room',
|
||||
sendTones: 'send-tones',
|
||||
setAudioOnly: 'set-audio-only',
|
||||
setAssumedBandwidthBps: 'set-assumed-bandwidth-bps',
|
||||
setBlurredBackground: 'set-blurred-background',
|
||||
setFollowMe: 'set-follow-me',
|
||||
@@ -103,6 +104,7 @@ const events = {
|
||||
'avatar-changed': 'avatarChanged',
|
||||
'audio-availability-changed': 'audioAvailabilityChanged',
|
||||
'audio-mute-status-changed': 'audioMuteStatusChanged',
|
||||
'audio-only-changed': 'audioOnlyChanged',
|
||||
'audio-or-video-sharing-toggled': 'audioOrVideoSharingToggled',
|
||||
'breakout-rooms-updated': 'breakoutRoomsUpdated',
|
||||
'browser-support': 'browserSupport',
|
||||
|
||||
@@ -211,14 +211,6 @@ UI.handleLastNEndpoints = function(leavingIds, enteringIds) {
|
||||
*/
|
||||
UI.setAudioLevel = (id, lvl) => VideoLayout.setAudioLevel(id, lvl);
|
||||
|
||||
/**
|
||||
* Update list of available physical devices.
|
||||
*/
|
||||
UI.onAvailableDevicesChanged = function() {
|
||||
APP.conference.updateAudioIconEnabled();
|
||||
APP.conference.updateVideoIconEnabled();
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the id of the current video shown on large.
|
||||
* Currently used by tests (torture).
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
"js-md5": "0.6.1",
|
||||
"js-sha512": "0.8.0",
|
||||
"jwt-decode": "2.2.0",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1876.0.0+fac989a9/lib-jitsi-meet.tgz",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet#release-8206",
|
||||
"lodash-es": "4.17.21",
|
||||
"moment": "2.29.4",
|
||||
"moment-duration-format": "2.2.2",
|
||||
|
||||
@@ -15,6 +15,7 @@ import { isAnalyticsEnabled } from '../base/lib-jitsi-meet/functions.any';
|
||||
import { getJitsiMeetGlobalNS } from '../base/util/helpers';
|
||||
import { inIframe } from '../base/util/iframeUtils';
|
||||
import { loadScript } from '../base/util/loadScript';
|
||||
import { parseURLParams } from '../base/util/parseURLParams';
|
||||
import { parseURIString } from '../base/util/uri';
|
||||
import { isPrejoinPageVisible } from '../prejoin/functions';
|
||||
|
||||
@@ -176,6 +177,7 @@ export function initAnalytics(store: IStore, handlers: Array<Object>): boolean {
|
||||
const { group, server } = state['features/base/jwt'];
|
||||
const { locationURL = { href: '' } } = state['features/base/connection'];
|
||||
const { tenant } = parseURIString(locationURL.href) || {};
|
||||
const params = parseURLParams(locationURL.href) ?? {};
|
||||
const permanentProperties: {
|
||||
appName?: string;
|
||||
externalApi?: boolean;
|
||||
@@ -183,6 +185,13 @@ export function initAnalytics(store: IStore, handlers: Array<Object>): boolean {
|
||||
inIframe?: boolean;
|
||||
isPromotedFromVisitor?: boolean;
|
||||
isVisitor?: boolean;
|
||||
overwritesDefaultLogoUrl?: boolean;
|
||||
overwritesDeploymentUrls?: boolean;
|
||||
overwritesLiveStreamingUrls?: boolean;
|
||||
overwritesPeopleSearchUrl?: boolean;
|
||||
overwritesPrejoinConfigICEUrl?: boolean;
|
||||
overwritesSalesforceUrl?: boolean;
|
||||
overwritesSupportUrl?: boolean;
|
||||
server?: string;
|
||||
tenant?: string;
|
||||
wasLobbyVisible?: boolean;
|
||||
@@ -221,6 +230,36 @@ export function initAnalytics(store: IStore, handlers: Array<Object>): boolean {
|
||||
permanentProperties.isVisitor = false;
|
||||
permanentProperties.isPromotedFromVisitor = false;
|
||||
|
||||
// TODO: Temporary metric. To be removed once we don't need it.
|
||||
permanentProperties.overwritesSupportUrl = 'interfaceConfig.SUPPORT_URL' in params;
|
||||
permanentProperties.overwritesSalesforceUrl = 'config.salesforceUrl' in params;
|
||||
permanentProperties.overwritesPeopleSearchUrl = 'config.peopleSearchUrl' in params;
|
||||
permanentProperties.overwritesDefaultLogoUrl = 'config.defaultLogoUrl' in params;
|
||||
const prejoinConfig = params['config.prejoinConfig'] ?? {};
|
||||
|
||||
permanentProperties.overwritesPrejoinConfigICEUrl = ('config.prejoinConfig.preCallTestICEUrl' in params)
|
||||
|| (typeof prejoinConfig === 'object' && 'preCallTestICEUrl' in prejoinConfig);
|
||||
const deploymentUrlsConfig = params['config.deploymentUrls'] ?? {};
|
||||
|
||||
permanentProperties.overwritesDeploymentUrls
|
||||
= 'config.deploymentUrls.downloadAppsUrl' in params || 'config.deploymentUrls.userDocumentationURL' in params
|
||||
|| (typeof deploymentUrlsConfig === 'object'
|
||||
&& ('downloadAppsUrl' in deploymentUrlsConfig || 'userDocumentationURL' in deploymentUrlsConfig));
|
||||
const liveStreamingConfig = params['config.liveStreaming'] ?? {};
|
||||
|
||||
permanentProperties.overwritesLiveStreamingUrls
|
||||
= ('interfaceConfig.LIVE_STREAMING_HELP_LINK' in params)
|
||||
|| ('config.liveStreaming.termsLink' in params)
|
||||
|| ('config.liveStreaming.dataPrivacyLink' in params)
|
||||
|| ('config.liveStreaming.helpLink' in params)
|
||||
|| (typeof params['config.liveStreaming'] === 'object' && 'config.liveStreaming' in params
|
||||
&& (
|
||||
'termsLink' in liveStreamingConfig
|
||||
|| 'dataPrivacyLink' in liveStreamingConfig
|
||||
|| 'helpLink' in liveStreamingConfig
|
||||
)
|
||||
);
|
||||
|
||||
// Optionally, include local deployment information based on the
|
||||
// contents of window.config.deploymentInfo.
|
||||
if (deploymentInfo) {
|
||||
|
||||
@@ -79,7 +79,7 @@ export default class BaseApp<P> extends Component<P, IState> {
|
||||
* @see {@link #_initStorage}
|
||||
* @type {Promise}
|
||||
*/
|
||||
this._init = createDeferred();
|
||||
this._init = createDeferred<void>();
|
||||
|
||||
try {
|
||||
await this._initStorage();
|
||||
|
||||
@@ -16,8 +16,9 @@ import { IStore } from '../../app/types';
|
||||
import { removeLobbyChatParticipant } from '../../chat/actions.any';
|
||||
import { openDisplayNamePrompt } from '../../display-name/actions';
|
||||
import { isVpaasMeeting } from '../../jaas/functions';
|
||||
import { showErrorNotification } from '../../notifications/actions';
|
||||
import { showErrorNotification, showNotification } from '../../notifications/actions';
|
||||
import { NOTIFICATION_TIMEOUT_TYPE } from '../../notifications/constants';
|
||||
import { INotificationProps } from '../../notifications/types';
|
||||
import { hasDisplayName } from '../../prejoin/utils';
|
||||
import { stopLocalVideoRecording } from '../../recording/actions.any';
|
||||
import LocalRecordingManager from '../../recording/components/Recording/LocalRecordingManager';
|
||||
@@ -37,6 +38,7 @@ import {
|
||||
import MiddlewareRegistry from '../redux/MiddlewareRegistry';
|
||||
import StateListenerRegistry from '../redux/StateListenerRegistry';
|
||||
import { TRACK_ADDED, TRACK_REMOVED } from '../tracks/actionTypes';
|
||||
import { parseURIString } from '../util/uri';
|
||||
|
||||
import {
|
||||
CONFERENCE_FAILED,
|
||||
@@ -419,6 +421,30 @@ function _connectionFailed({ dispatch, getState }: IStore, next: Function, actio
|
||||
}
|
||||
}
|
||||
|
||||
if (error.name === JitsiConnectionErrors.CONFERENCE_REQUEST_FAILED) {
|
||||
let notificationAction: Function = showNotification;
|
||||
const notificationProps = {
|
||||
customActionNameKey: [ 'dialog.rejoinNow' ],
|
||||
customActionHandler: [ () => dispatch(reloadNow()) ],
|
||||
descriptionKey: 'notify.connectionFailed'
|
||||
} as INotificationProps;
|
||||
|
||||
const { locationURL = { href: '' } as URL } = getState()['features/base/connection'];
|
||||
const { tenant } = parseURIString(locationURL.href) || {};
|
||||
|
||||
if (tenant.startsWith('-') || tenant.endsWith('-')) {
|
||||
notificationProps.descriptionKey = 'notify.invalidTenantHyphenDescription';
|
||||
notificationProps.titleKey = 'notify.invalidTenant';
|
||||
notificationAction = showErrorNotification;
|
||||
} else if (tenant.length > 63) {
|
||||
notificationProps.descriptionKey = 'notify.invalidTenantLengthDescription';
|
||||
notificationProps.titleKey = 'notify.invalidTenant';
|
||||
notificationAction = showErrorNotification;
|
||||
}
|
||||
|
||||
dispatch(notificationAction(notificationProps, NOTIFICATION_TIMEOUT_TYPE.STICKY));
|
||||
}
|
||||
|
||||
const result = next(action);
|
||||
|
||||
_removeUnloadHandler(getState);
|
||||
@@ -614,37 +640,6 @@ function _setRoom({ dispatch, getState }: IStore, next: Function, action: AnyAct
|
||||
return next(action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronizes local tracks from state with local tracks in JitsiConference
|
||||
* instance.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @param {Object} action - Action object.
|
||||
* @private
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function _syncConferenceLocalTracksWithState({ getState }: IStore, action: AnyAction) {
|
||||
const state = getState();
|
||||
const conference = getCurrentConference(state);
|
||||
let promise;
|
||||
|
||||
if (conference) {
|
||||
const track = action.track.jitsiTrack;
|
||||
|
||||
if (action.type === TRACK_ADDED) {
|
||||
// If gUM is slow and tracks are created after the user has already joined the conference, avoid
|
||||
// adding the tracks to the conference if the user is a visitor.
|
||||
if (!iAmVisitor(state)) {
|
||||
promise = _addLocalTracksToConference(conference, [ track ]);
|
||||
}
|
||||
} else {
|
||||
promise = _removeLocalTracksFromConference(conference, [ track ]);
|
||||
}
|
||||
}
|
||||
|
||||
return promise || Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the feature base/conference that the action {@code TRACK_ADDED}
|
||||
* or {@code TRACK_REMOVED} is being dispatched within a specific redux store.
|
||||
@@ -664,9 +659,28 @@ function _trackAddedOrRemoved(store: IStore, next: Function, action: AnyAction)
|
||||
|
||||
// TODO All track swapping should happen here instead of conference.js.
|
||||
if (track?.local) {
|
||||
return (
|
||||
_syncConferenceLocalTracksWithState(store, action)
|
||||
.then(() => next(action)));
|
||||
const { getState } = store;
|
||||
const state = getState();
|
||||
const conference = getCurrentConference(state);
|
||||
let promise;
|
||||
|
||||
if (conference) {
|
||||
const jitsiTrack = action.track.jitsiTrack;
|
||||
|
||||
if (action.type === TRACK_ADDED) {
|
||||
// If gUM is slow and tracks are created after the user has already joined the conference, avoid
|
||||
// adding the tracks to the conference if the user is a visitor.
|
||||
if (!iAmVisitor(state)) {
|
||||
promise = _addLocalTracksToConference(conference, [ jitsiTrack ]);
|
||||
}
|
||||
} else {
|
||||
promise = _removeLocalTracksFromConference(conference, [ jitsiTrack ]);
|
||||
}
|
||||
|
||||
if (promise) {
|
||||
return promise.then(() => next(action));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return next(action);
|
||||
|
||||
@@ -149,12 +149,15 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
break;
|
||||
}
|
||||
case CONNECTION_ESTABLISHED: {
|
||||
if (isPrejoinPageVisible(getState())) {
|
||||
let { initialGUMPromise } = getState()['features/base/media'];
|
||||
const { initialGUMPromise } = getState()['features/base/media'];
|
||||
const promise = initialGUMPromise ? initialGUMPromise.promise : Promise.resolve({ tracks: [] });
|
||||
const prejoinVisible = isPrejoinPageVisible(getState());
|
||||
|
||||
initialGUMPromise = initialGUMPromise || Promise.resolve({ tracks: [] });
|
||||
logger.debug(`On connection established: prejoinVisible: ${prejoinVisible}, initialGUMPromiseExists=${
|
||||
Boolean(initialGUMPromise)}, promiseExists=${Boolean(promise)}`);
|
||||
|
||||
initialGUMPromise.then(() => {
|
||||
if (prejoinVisible) {
|
||||
promise.then(() => {
|
||||
const state = getState();
|
||||
let localTracks = getLocalTracks(state['features/base/tracks']);
|
||||
const trackReplacePromises = [];
|
||||
@@ -186,11 +189,7 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
});
|
||||
});
|
||||
} else {
|
||||
let { initialGUMPromise } = getState()['features/base/media'];
|
||||
|
||||
initialGUMPromise = initialGUMPromise || Promise.resolve({ tracks: [] });
|
||||
|
||||
initialGUMPromise.then(({ tracks }) => {
|
||||
promise.then(({ tracks }) => {
|
||||
let tracksToUse = tracks ?? [];
|
||||
|
||||
if (iAmVisitor(getState())) {
|
||||
|
||||
@@ -484,6 +484,8 @@ export interface IConfig {
|
||||
enabled?: boolean;
|
||||
hideDisplayName?: boolean;
|
||||
hideExtraJoinButtons?: Array<string>;
|
||||
preCallTestEnabled?: boolean;
|
||||
preCallTestICEUrl?: string;
|
||||
};
|
||||
prejoinPageEnabled?: boolean;
|
||||
raisedHands?: {
|
||||
@@ -574,6 +576,7 @@ export interface IConfig {
|
||||
transcribeWithAppLanguage?: boolean;
|
||||
transcribingEnabled?: boolean;
|
||||
transcription?: {
|
||||
autoCaptionOnTranscribe?: boolean;
|
||||
autoTranscribeOnRecord?: boolean;
|
||||
enabled?: boolean;
|
||||
preferredLanguage?: string;
|
||||
|
||||
@@ -84,7 +84,6 @@ export default [
|
||||
'deeplinking.desktop.enabled',
|
||||
'defaultLocalDisplayName',
|
||||
'defaultRemoteDisplayName',
|
||||
'deploymentUrls',
|
||||
'desktopSharingFrameRate',
|
||||
'desktopSharingSources',
|
||||
'disable1On1Mode',
|
||||
@@ -179,7 +178,7 @@ export default [
|
||||
'iceTransportPolicy',
|
||||
'ignoreStartMuted',
|
||||
'inviteAppName',
|
||||
'liveStreaming',
|
||||
'liveStreaming.enabled',
|
||||
'liveStreamingEnabled',
|
||||
'lobby',
|
||||
'localRecording',
|
||||
@@ -198,7 +197,9 @@ export default [
|
||||
'pcStatsInterval',
|
||||
'preferBosh',
|
||||
'preferVisitor',
|
||||
'prejoinConfig',
|
||||
'prejoinConfig.enabled',
|
||||
'prejoinConfig.hideDisplayName',
|
||||
'prejoinConfig.hideExtraJoinButtons',
|
||||
'prejoinPageEnabled',
|
||||
'raisedHands',
|
||||
'recordingService',
|
||||
@@ -209,7 +210,6 @@ export default [
|
||||
'recordings',
|
||||
'replaceParticipant',
|
||||
'resolution',
|
||||
'salesforceUrl',
|
||||
'screenshotCapture',
|
||||
'securityUi',
|
||||
'speakerStats',
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Deploy-specific interface_config whitelists.
|
||||
*/
|
||||
export default [];
|
||||
@@ -1,3 +1,5 @@
|
||||
import extraInterfaceConfigWhitelistCopy from './extraInterfaceConfigWhitelist';
|
||||
|
||||
/**
|
||||
* The interface config keys to whitelist, the keys that can be overridden.
|
||||
*
|
||||
@@ -32,7 +34,6 @@ export default [
|
||||
'INDICATOR_FONT_SIZES',
|
||||
'INITIAL_TOOLBAR_TIMEOUT',
|
||||
'LANG_DETECTION',
|
||||
'LIVE_STREAMING_HELP_LINK',
|
||||
'LOCAL_THUMBNAIL_RATIO',
|
||||
'MAXIMUM_ZOOMING_COEFFICIENT',
|
||||
'NATIVE_APP_NAME',
|
||||
@@ -45,7 +46,6 @@ export default [
|
||||
'SHARING_FEATURES',
|
||||
'SHOW_CHROME_EXTENSION_BANNER',
|
||||
'SHOW_POWERED_BY',
|
||||
'SUPPORT_URL',
|
||||
'TILE_VIEW_MAX_COLUMNS',
|
||||
'TOOLBAR_ALWAYS_VISIBLE',
|
||||
'TOOLBAR_BUTTONS',
|
||||
@@ -54,4 +54,4 @@ export default [
|
||||
'VERTICAL_FILMSTRIP',
|
||||
'VIDEO_LAYOUT_FIT',
|
||||
'VIDEO_QUALITY_LABEL_DISABLED'
|
||||
];
|
||||
].concat(extraInterfaceConfigWhitelistCopy);
|
||||
|
||||
@@ -75,6 +75,9 @@ export function configureInitialDevices() {
|
||||
const deviceLabels = getDevicesFromURL(getState());
|
||||
let updateSettingsPromise;
|
||||
|
||||
logger.debug(`(TIME) configureInitialDevices: deviceLabels=${
|
||||
Boolean(deviceLabels)}, performance.now=${window.performance.now()}`);
|
||||
|
||||
if (deviceLabels) {
|
||||
updateSettingsPromise = dispatch(getAvailableDevices()).then(() => {
|
||||
const state = getState();
|
||||
@@ -127,6 +130,9 @@ export function configureInitialDevices() {
|
||||
.then(() => {
|
||||
const userSelectedAudioOutputDeviceId = getUserSelectedOutputDeviceId(getState());
|
||||
|
||||
logger.debug(`(TIME) configureInitialDevices -> setAudioOutputDeviceId: performance.now=${
|
||||
window.performance.now()}`);
|
||||
|
||||
return setAudioOutputDeviceId(userSelectedAudioOutputDeviceId, dispatch)
|
||||
.catch(ex => logger.warn(`Failed to set audio output device.
|
||||
Default audio output device will be used instead ${ex}`));
|
||||
|
||||
@@ -3,6 +3,7 @@ import { AnyAction, combineReducers } from 'redux';
|
||||
import { CONFERENCE_FAILED, CONFERENCE_LEFT } from '../conference/actionTypes';
|
||||
import ReducerRegistry from '../redux/ReducerRegistry';
|
||||
import { TRACK_REMOVED } from '../tracks/actionTypes';
|
||||
import { DefferedPromise, createDeferred } from '../util/helpers';
|
||||
|
||||
import {
|
||||
GUM_PENDING,
|
||||
@@ -88,6 +89,12 @@ function _audio(state: IAudioState = _AUDIO_INITIAL_MEDIA_STATE, action: AnyActi
|
||||
}
|
||||
}
|
||||
|
||||
// Using a deferred promise here to make sure that once the connection is established even if conference.init and the
|
||||
// initial track creation haven't been started we would wait for it to finish before starting to join the room.
|
||||
// NOTE: The previous implementation was using the GUM promise from conference.init. But it turned out that connect
|
||||
// may finish even before conference.init is executed.
|
||||
const DEFAULT_INITIAL_PROMISE_STATE = createDeferred<IInitialGUMPromiseResult>();
|
||||
|
||||
/**
|
||||
* Reducer fot the common properties in media state.
|
||||
*
|
||||
@@ -96,7 +103,8 @@ function _audio(state: IAudioState = _AUDIO_INITIAL_MEDIA_STATE, action: AnyActi
|
||||
* @param {string} action.type - Type of action.
|
||||
* @returns {ICommonState}
|
||||
*/
|
||||
function _initialGUMPromise(state: initialGUMPromise | null = null, action: AnyAction) {
|
||||
function _initialGUMPromise(state: DefferedPromise<IInitialGUMPromiseResult> | null = DEFAULT_INITIAL_PROMISE_STATE,
|
||||
action: AnyAction) {
|
||||
if (action.type === SET_INITIAL_GUM_PROMISE) {
|
||||
return action.promise ?? null;
|
||||
}
|
||||
@@ -264,10 +272,10 @@ interface IAudioState {
|
||||
unmuteBlocked: boolean;
|
||||
}
|
||||
|
||||
type initialGUMPromise = Promise<{
|
||||
errors?: any;
|
||||
tracks: Array<any>;
|
||||
}> | null;
|
||||
interface IInitialGUMPromiseResult {
|
||||
errors?: any;
|
||||
tracks: Array<any>;
|
||||
}
|
||||
|
||||
interface IScreenshareState {
|
||||
available: boolean;
|
||||
@@ -286,7 +294,7 @@ interface IVideoState {
|
||||
|
||||
export interface IMediaState {
|
||||
audio: IAudioState;
|
||||
initialGUMPromise: initialGUMPromise;
|
||||
initialGUMPromise: DefferedPromise<IInitialGUMPromiseResult> | null;
|
||||
screenshare: IScreenshareState;
|
||||
video: IVideoState;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
|
||||
/**
|
||||
* Action type to set the precall test data.
|
||||
*/
|
||||
export const SET_PRECALL_TEST_RESULTS = 'SET_PRECALL_TEST_RESULTS';
|
||||
|
||||
/**
|
||||
* Type for setting the user's consent for unsafe room joining.
|
||||
*
|
||||
@@ -6,4 +12,4 @@
|
||||
* consent: boolean
|
||||
* }
|
||||
*/
|
||||
export const SET_UNSAFE_ROOM_CONSENT = 'SET_UNSAFE_ROOM_CONSENT'
|
||||
export const SET_UNSAFE_ROOM_CONSENT = 'SET_UNSAFE_ROOM_CONSENT'
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { SET_UNSAFE_ROOM_CONSENT } from './actionTypes';
|
||||
import { IStore } from '../../app/types';
|
||||
import JitsiMeetJS from '../lib-jitsi-meet';
|
||||
|
||||
import { SET_PRECALL_TEST_RESULTS, SET_UNSAFE_ROOM_CONSENT } from './actionTypes';
|
||||
import { getPreCallICEUrl } from './functions';
|
||||
import logger from './logger';
|
||||
import { IPreCallResult, IPreCallTestState, PreCallTestStatus } from './types';
|
||||
|
||||
/**
|
||||
* Sets the consent of the user for joining the unsafe room.
|
||||
@@ -15,3 +21,47 @@ export function setUnsafeRoomConsent(consent: boolean) {
|
||||
consent
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the 'precallTest' and executes one test, storing the results.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function runPreCallTest() {
|
||||
return async function(dispatch: Function, getState: IStore['getState']) {
|
||||
try {
|
||||
|
||||
dispatch(setPreCallTestResults({ status: PreCallTestStatus.RUNNING }));
|
||||
|
||||
const turnCredentialsUrl = getPreCallICEUrl(getState());
|
||||
|
||||
if (!turnCredentialsUrl) {
|
||||
throw new Error('No TURN credentials URL provided in config');
|
||||
}
|
||||
|
||||
const turnCredentials = await fetch(turnCredentialsUrl);
|
||||
const { iceServers } = await turnCredentials.json();
|
||||
const result: IPreCallResult = await JitsiMeetJS.runPreCallTest(iceServers);
|
||||
|
||||
dispatch(setPreCallTestResults({ status: PreCallTestStatus.FINISHED,
|
||||
result }));
|
||||
} catch (error) {
|
||||
logger.error('Failed to run pre-call test', error);
|
||||
|
||||
dispatch(setPreCallTestResults({ status: PreCallTestStatus.FAILED }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action used to set data from precall test.
|
||||
*
|
||||
* @param {IPreCallTestState} value - The precall test results.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function setPreCallTestResults(value: IPreCallTestState) {
|
||||
return {
|
||||
type: SET_PRECALL_TEST_RESULTS,
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,29 +1,17 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { translate } from '../../../i18n/functions';
|
||||
import Icon from '../../../icons/components/Icon';
|
||||
import { IconArrowDown, IconWifi1Bar, IconWifi2Bars, IconWifi3Bars } from '../../../icons/svg';
|
||||
import { IconArrowDown, IconCloseCircle, IconWifi1Bar, IconWifi2Bars, IconWifi3Bars } from '../../../icons/svg';
|
||||
import { withPixelLineHeight } from '../../../styles/functions.web';
|
||||
import { PREJOIN_DEFAULT_CONTENT_WIDTH } from '../../../ui/components/variables';
|
||||
import Spinner from '../../../ui/components/web/Spinner';
|
||||
import { runPreCallTest } from '../../actions.web';
|
||||
import { CONNECTION_TYPE } from '../../constants';
|
||||
import { getConnectionData } from '../../functions';
|
||||
|
||||
interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* List of strings with details about the connection.
|
||||
*/
|
||||
connectionDetails?: string[];
|
||||
|
||||
/**
|
||||
* The type of the connection. Can be: 'none', 'poor', 'nonOptimal' or 'good'.
|
||||
*/
|
||||
connectionType?: string;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
connectionStatus: {
|
||||
@@ -68,6 +56,10 @@ const useStyles = makeStyles()(theme => {
|
||||
background: '#31B76A'
|
||||
},
|
||||
|
||||
'& .con-status--failed': {
|
||||
background: '#E12D2D'
|
||||
},
|
||||
|
||||
'& .con-status--poor': {
|
||||
background: '#E12D2D'
|
||||
},
|
||||
@@ -122,6 +114,11 @@ const CONNECTION_TYPE_MAP: {
|
||||
icon: Function;
|
||||
};
|
||||
} = {
|
||||
[CONNECTION_TYPE.FAILED]: {
|
||||
connectionClass: 'con-status--failed',
|
||||
icon: IconCloseCircle,
|
||||
connectionText: 'prejoin.connection.failed'
|
||||
},
|
||||
[CONNECTION_TYPE.POOR]: {
|
||||
connectionClass: 'con-status--poor',
|
||||
icon: IconWifi1Bar,
|
||||
@@ -145,10 +142,17 @@ const CONNECTION_TYPE_MAP: {
|
||||
* @param {IProps} props - The props of the component.
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
function ConnectionStatus({ connectionDetails, t, connectionType }: IProps) {
|
||||
const ConnectionStatus = () => {
|
||||
const { classes } = useStyles();
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const { connectionType, connectionDetails } = useSelector(getConnectionData);
|
||||
const [ showDetails, toggleDetails ] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(runPreCallTest());
|
||||
}, []);
|
||||
|
||||
const arrowClassName = showDetails
|
||||
? 'con-status-arrow con-status-arrow--up'
|
||||
: 'con-status-arrow';
|
||||
@@ -173,6 +177,26 @@ function ConnectionStatus({ connectionDetails, t, connectionType }: IProps) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (connectionType === CONNECTION_TYPE.RUNNING) {
|
||||
return (
|
||||
<div className = { classes.connectionStatus }>
|
||||
<div
|
||||
aria-level = { 1 }
|
||||
className = 'con-status-header'
|
||||
role = 'heading'>
|
||||
<div className = 'con-status-circle'>
|
||||
<Spinner
|
||||
color = { 'green' }
|
||||
size = 'medium' />
|
||||
</div>
|
||||
<span
|
||||
className = 'con-status-text'
|
||||
id = 'connection-status-description'>{t('prejoin.connection.running')}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const { connectionClass, icon, connectionText } = CONNECTION_TYPE_MAP[connectionType ?? ''];
|
||||
|
||||
return (
|
||||
@@ -208,21 +232,6 @@ function ConnectionStatus({ connectionDetails, t, connectionType }: IProps) {
|
||||
{detailsText}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps (parts of) the redux state to the React {@code Component} props.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {Object}
|
||||
*/
|
||||
function mapStateToProps() {
|
||||
const { connectionDetails, connectionType } = getConnectionData();
|
||||
|
||||
return {
|
||||
connectionDetails,
|
||||
connectionType
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(mapStateToProps)(ConnectionStatus));
|
||||
export default ConnectionStatus;
|
||||
|
||||
@@ -11,6 +11,7 @@ import { isButtonEnabled } from '../../../../toolbox/functions.web';
|
||||
import { getConferenceName } from '../../../conference/functions';
|
||||
import { PREMEETING_BUTTONS, THIRD_PARTY_PREJOIN_BUTTONS } from '../../../config/constants';
|
||||
import { withPixelLineHeight } from '../../../styles/functions.web';
|
||||
import { isPreCallTestEnabled } from '../../functions';
|
||||
|
||||
import ConnectionStatus from './ConnectionStatus';
|
||||
import Preview from './Preview';
|
||||
@@ -24,6 +25,11 @@ interface IProps {
|
||||
*/
|
||||
_buttons: Array<string>;
|
||||
|
||||
/**
|
||||
* Determine if pre call test is enabled.
|
||||
*/
|
||||
_isPreCallTestEnabled?: boolean;
|
||||
|
||||
/**
|
||||
* The branding background of the premeeting screen(lobby/prejoin).
|
||||
*/
|
||||
@@ -169,6 +175,7 @@ const useStyles = makeStyles()(theme => {
|
||||
|
||||
const PreMeetingScreen = ({
|
||||
_buttons,
|
||||
_isPreCallTestEnabled,
|
||||
_premeetingBackground,
|
||||
_roomName,
|
||||
children,
|
||||
@@ -188,11 +195,13 @@ const PreMeetingScreen = ({
|
||||
backgroundSize: 'cover'
|
||||
} : {};
|
||||
|
||||
console.log('Rendering premeeting....');
|
||||
|
||||
return (
|
||||
<div className = { clsx('premeeting-screen', classes.container, className) }>
|
||||
<div style = { style }>
|
||||
<div className = { classes.content }>
|
||||
<ConnectionStatus />
|
||||
{_isPreCallTestEnabled && <ConnectionStatus />}
|
||||
|
||||
<div className = { classes.contentControls }>
|
||||
<h1 className = { classes.title }>
|
||||
@@ -245,6 +254,7 @@ function mapStateToProps(state: IReduxState, ownProps: Partial<IProps>) {
|
||||
_buttons: hiddenPremeetingButtons
|
||||
? premeetingButtons
|
||||
: premeetingButtons.filter(b => isButtonEnabled(b, toolbarButtons)),
|
||||
_isPreCallTestEnabled: isPreCallTestEnabled(state),
|
||||
_premeetingBackground: premeetingBackground,
|
||||
_roomName: isRoomNameEnabled(state) ? getConferenceName(state) : ''
|
||||
};
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
export const CONNECTION_TYPE = {
|
||||
FAILED: 'failed',
|
||||
GOOD: 'good',
|
||||
NON_OPTIMAL: 'nonOptimal',
|
||||
NONE: 'none',
|
||||
POOR: 'poor'
|
||||
POOR: 'poor',
|
||||
RUNNING: 'running'
|
||||
};
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { findIndex } from 'lodash-es';
|
||||
|
||||
import { IReduxState } from '../../app/types';
|
||||
|
||||
import { CONNECTION_TYPE } from './constants';
|
||||
import logger from './logger';
|
||||
import { IPreCallResult, PreCallTestStatus } from './types';
|
||||
|
||||
|
||||
/**
|
||||
@@ -31,6 +37,14 @@ const defaultMarginTop = '10%';
|
||||
*/
|
||||
const smallMarginTop = '5%';
|
||||
|
||||
// loss in percentage overall the test duration
|
||||
const LOSS_AUDIO_THRESHOLDS = [ 0.33, 0.05 ];
|
||||
const LOSS_VIDEO_THRESHOLDS = [ 0.33, 0.1, 0.05 ];
|
||||
|
||||
// throughput in kbps
|
||||
const THROUGHPUT_AUDIO_THRESHOLDS = [ 8, 20 ];
|
||||
const THROUGHPUT_VIDEO_THRESHOLDS = [ 60, 750 ];
|
||||
|
||||
/**
|
||||
* Calculates avatar dimensions based on window height and position.
|
||||
*
|
||||
@@ -71,16 +85,187 @@ export function calculateAvatarDimensions(height: number) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector for determining the connection type & details.
|
||||
* 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: number[], value: number, descending = true) {
|
||||
let predicate;
|
||||
|
||||
if (descending) {
|
||||
predicate = function(threshold: number) {
|
||||
return value > threshold;
|
||||
};
|
||||
} else {
|
||||
predicate = function(threshold: number) {
|
||||
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 {number} testResults.fractionalLoss - Factional loss.
|
||||
* @param {number} testResults.throughput - Throughput.
|
||||
*
|
||||
* @returns {{
|
||||
* connectionType: string,
|
||||
* connectionDetails: string[]
|
||||
* }}
|
||||
*/
|
||||
export function getConnectionData() {
|
||||
* connectionType: string,
|
||||
* connectionDetails: string[]
|
||||
* }}
|
||||
*/
|
||||
function _getConnectionDataFromTestResults({ fractionalLoss: l, throughput: t, mediaConnectivity }: IPreCallResult) {
|
||||
let connectionType = CONNECTION_TYPE.FAILED;
|
||||
const connectionDetails: Array<string> = [];
|
||||
|
||||
if (!mediaConnectivity) {
|
||||
connectionType = CONNECTION_TYPE.POOR;
|
||||
connectionDetails.push('prejoin.connectionDetails.noMediaConnectivity');
|
||||
|
||||
return {
|
||||
connectionType,
|
||||
connectionDetails
|
||||
};
|
||||
}
|
||||
|
||||
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)
|
||||
};
|
||||
|
||||
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: CONNECTION_TYPE.NONE,
|
||||
connectionDetails: []
|
||||
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: IReduxState) {
|
||||
const { preCallTestState: { status, result } } = state['features/base/premeeting'];
|
||||
|
||||
switch (status) {
|
||||
case PreCallTestStatus.INITIAL:
|
||||
return {
|
||||
connectionType: CONNECTION_TYPE.NONE,
|
||||
connectionDetails: []
|
||||
};
|
||||
case PreCallTestStatus.RUNNING:
|
||||
return {
|
||||
connectionType: CONNECTION_TYPE.RUNNING,
|
||||
connectionDetails: []
|
||||
};
|
||||
case PreCallTestStatus.FAILED:
|
||||
// A failed test means that something went wrong with our business logic and not necessarily
|
||||
// that the connection is bad. For instance, the endpoint providing the ICE credentials could be down.
|
||||
return {
|
||||
connectionType: CONNECTION_TYPE.FAILED,
|
||||
connectionDetails: [ 'prejoin.connectionDetails.testFailed' ]
|
||||
};
|
||||
case PreCallTestStatus.FINISHED:
|
||||
if (result) {
|
||||
return _getConnectionDataFromTestResults(result);
|
||||
}
|
||||
|
||||
logger.error('Pre-call test finished but no test results were available');
|
||||
|
||||
return {
|
||||
connectionType: CONNECTION_TYPE.FAILED,
|
||||
connectionDetails: [ 'prejoin.connectionDetails.testFailed' ]
|
||||
};
|
||||
default:
|
||||
return {
|
||||
connectionType: CONNECTION_TYPE.NONE,
|
||||
connectionDetails: []
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector for determining if the pre-call test is enabled.
|
||||
*
|
||||
* @param {Object} state - The state of the app.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isPreCallTestEnabled(state: IReduxState): boolean {
|
||||
const { prejoinConfig } = state['features/base/config'];
|
||||
|
||||
return prejoinConfig?.preCallTestEnabled ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector for retrieving the pre-call test ICE URL.
|
||||
*
|
||||
* @param {Object} state - The state of the app.
|
||||
* @returns {string | undefined}
|
||||
*/
|
||||
export function getPreCallICEUrl(state: IReduxState): string | undefined {
|
||||
const { prejoinConfig } = state['features/base/config'];
|
||||
|
||||
return prejoinConfig?.preCallTestICEUrl;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import ReducerRegistry from '../redux/ReducerRegistry';
|
||||
|
||||
import { SET_UNSAFE_ROOM_CONSENT } from './actionTypes';
|
||||
import { IPreMeetingState } from './types';
|
||||
import { SET_PRECALL_TEST_RESULTS, SET_UNSAFE_ROOM_CONSENT } from './actionTypes';
|
||||
import { IPreMeetingState, PreCallTestStatus } from './types';
|
||||
|
||||
|
||||
const DEFAULT_STATE: IPreMeetingState = {
|
||||
preCallTestState: {
|
||||
status: PreCallTestStatus.INITIAL
|
||||
},
|
||||
unsafeRoomConsent: false
|
||||
};
|
||||
|
||||
@@ -20,6 +23,12 @@ ReducerRegistry.register<IPreMeetingState>(
|
||||
'features/base/premeeting',
|
||||
(state = DEFAULT_STATE, action): IPreMeetingState => {
|
||||
switch (action.type) {
|
||||
case SET_PRECALL_TEST_RESULTS:
|
||||
return {
|
||||
...state,
|
||||
preCallTestState: action.value
|
||||
};
|
||||
|
||||
case SET_UNSAFE_ROOM_CONSENT: {
|
||||
return {
|
||||
...state,
|
||||
|
||||
@@ -1,3 +1,25 @@
|
||||
|
||||
export enum PreCallTestStatus {
|
||||
FAILED = 'FAILED',
|
||||
FINISHED = 'FINISHED',
|
||||
INITIAL = 'INITIAL',
|
||||
RUNNING = 'RUNNING'
|
||||
}
|
||||
|
||||
export interface IPreMeetingState {
|
||||
preCallTestState: IPreCallTestState;
|
||||
unsafeRoomConsent?: boolean;
|
||||
}
|
||||
|
||||
export interface IPreCallTestState {
|
||||
result?: IPreCallResult;
|
||||
status: PreCallTestStatus;
|
||||
}
|
||||
|
||||
export interface IPreCallResult {
|
||||
fractionalLoss: number;
|
||||
jitter: number;
|
||||
mediaConnectivity: boolean;
|
||||
rtt: number;
|
||||
throughput: number;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { withPixelLineHeight } from '../../../styles/functions.web';
|
||||
import Button from '../../../ui/components/web/Button';
|
||||
import { getSupportUrl } from '../../functions';
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
@@ -50,7 +52,7 @@ const InlineDialogFailure = ({
|
||||
const { t } = useTranslation();
|
||||
const { classes } = useStyles();
|
||||
|
||||
const supportLink = interfaceConfig.SUPPORT_URL;
|
||||
const supportLink = useSelector(getSupportUrl);
|
||||
const supportString = t('inlineDialogFailure.supportMsg');
|
||||
const supportLinkElem = supportLink && showSupportLink
|
||||
? (
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import punycode from 'punycode';
|
||||
|
||||
import { IStateful } from '../app/types';
|
||||
import { toState } from '../redux/functions';
|
||||
|
||||
/**
|
||||
* Returns the field value in a platform generic way.
|
||||
*
|
||||
@@ -47,3 +50,16 @@ export function formatURLText(text = '') {
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the configured support URL.
|
||||
*
|
||||
* @param {IStateful} stateful - The redux state.
|
||||
* @returns {string|undefined} - The configured support link.
|
||||
*/
|
||||
export function getSupportUrl(stateful: IStateful) {
|
||||
|
||||
// TODO: Once overwriting trough interface config is completelly gone we should think of a way to be able to set
|
||||
// the value in the branding and not return the default value from interface config.
|
||||
return toState(stateful)['features/dynamic-branding'].supportUrl || interfaceConfig?.SUPPORT_URL;
|
||||
}
|
||||
|
||||
@@ -352,9 +352,10 @@ export function createAndAddInitialAVTracks(devices: Array<MediaType>) {
|
||||
* Creates the initial audio/video tracks.
|
||||
*
|
||||
* @param {ICreateInitialTracksOptions} options - Options for creating the audio/video tracks.
|
||||
* @param {boolean} recordTimeMetrics - If true time metrics will be recorded.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function createInitialAVTracks(options: ICreateInitialTracksOptions) {
|
||||
export function createInitialAVTracks(options: ICreateInitialTracksOptions, recordTimeMetrics = false) {
|
||||
return (dispatch: IStore['dispatch'], _getState: IStore['getState']) => {
|
||||
const {
|
||||
devices,
|
||||
@@ -364,7 +365,7 @@ export function createInitialAVTracks(options: ICreateInitialTracksOptions) {
|
||||
|
||||
dispatch(gumPending(devices, IGUMPendingState.PENDING_UNMUTE));
|
||||
|
||||
return createLocalTracksF(options).then(tracks => {
|
||||
return createLocalTracksF(options, undefined, recordTimeMetrics).then(tracks => {
|
||||
return {
|
||||
errors: {} as IInitialTracksErrors,
|
||||
tracks
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
getUserSelectedCameraDeviceId,
|
||||
getUserSelectedMicDeviceId
|
||||
} from '../settings/functions.web';
|
||||
import { getJitsiMeetGlobalNSConnectionTimes } from '../util/helpers';
|
||||
|
||||
import { getCameraFacingMode } from './functions.any';
|
||||
import loadEffects from './loadEffects';
|
||||
@@ -36,9 +37,10 @@ export * from './functions.any';
|
||||
* corresponding event.
|
||||
* @param {IStore} store - The redux store in the context of which the function
|
||||
* is to execute and from which state such as {@code config} is to be retrieved.
|
||||
* @param {boolean} recordTimeMetrics - If true time metrics will be recorded.
|
||||
* @returns {Promise<JitsiLocalTrack[]>}
|
||||
*/
|
||||
export function createLocalTracksF(options: ITrackOptions = {}, store?: IStore) {
|
||||
export function createLocalTracksF(options: ITrackOptions = {}, store?: IStore, recordTimeMetrics = false) {
|
||||
let { cameraDeviceId, micDeviceId } = options;
|
||||
const {
|
||||
desktopSharingSourceDevice,
|
||||
@@ -69,6 +71,10 @@ export function createLocalTracksF(options: ITrackOptions = {}, store?: IStore)
|
||||
|
||||
return (
|
||||
loadEffects(store).then((effectsArray: Object[]) => {
|
||||
if (recordTimeMetrics) {
|
||||
getJitsiMeetGlobalNSConnectionTimes()['trackEffects.loaded'] = window.performance.now();
|
||||
}
|
||||
|
||||
// Filter any undefined values returned by Promise.resolve().
|
||||
const effects = effectsArray.filter(effect => Boolean(effect));
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import logger from './logger';
|
||||
* @returns {Promise} - A Promise which resolves when all effects are created.
|
||||
*/
|
||||
export default function loadEffects(store: IStore): Promise<any> {
|
||||
const start = window.performance.now();
|
||||
const state = store.getState();
|
||||
const virtualBackground = state['features/virtual-background'];
|
||||
const noiseSuppression = state['features/noise-suppression'];
|
||||
@@ -30,5 +31,11 @@ export default function loadEffects(store: IStore): Promise<any> {
|
||||
? Promise.resolve(new NoiseSuppressionEffect(nsOptions))
|
||||
: Promise.resolve();
|
||||
|
||||
return Promise.all([ backgroundPromise, noiseSuppressionPromise ]);
|
||||
return Promise.all([ backgroundPromise, noiseSuppressionPromise ]).then(effectsArray => {
|
||||
const end = window.performance.now();
|
||||
|
||||
logger.debug(`(TIME) loadEffects() start=${start}, end=${end}, time=${end - start}`);
|
||||
|
||||
return effectsArray;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -22,16 +22,21 @@ export function assignIfDefined(target: Object, source: Object) {
|
||||
return to;
|
||||
}
|
||||
|
||||
export type DefferedPromise<T> = {
|
||||
promise: Promise<T>;
|
||||
reject: (reason?: any) => void;
|
||||
resolve: (value: T) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a deferred object.
|
||||
*
|
||||
* @returns {{promise, resolve, reject}}
|
||||
*/
|
||||
export function createDeferred() {
|
||||
const deferred: any = {};
|
||||
export function createDeferred<T>() {
|
||||
const deferred = {} as DefferedPromise<T>;
|
||||
|
||||
deferred.promise = new Promise((resolve, reject) => {
|
||||
deferred.promise = new Promise<T>((resolve, reject) => {
|
||||
deferred.resolve = resolve;
|
||||
deferred.reject = reject;
|
||||
});
|
||||
@@ -95,6 +100,21 @@ export function getJitsiMeetGlobalNS() {
|
||||
return window.JitsiMeetJS.app;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the object that stores the connection times.
|
||||
*
|
||||
* @returns {Object} - The object that stores the connection times.
|
||||
*/
|
||||
export function getJitsiMeetGlobalNSConnectionTimes() {
|
||||
const globalNS = getJitsiMeetGlobalNS();
|
||||
|
||||
if (!globalNS.connectionTimes) {
|
||||
globalNS.connectionTimes = {};
|
||||
}
|
||||
|
||||
return globalNS.connectionTimes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints the error and reports it to the global error handler.
|
||||
*
|
||||
|
||||
@@ -127,11 +127,14 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
const { participant, data } = action;
|
||||
|
||||
if (data?.name === ENDPOINT_REACTION_NAME) {
|
||||
store.dispatch(pushReactions(data.reactions));
|
||||
// Skip duplicates, keep just 3.
|
||||
const reactions = Array.from(new Set(data.reactions)).slice(0, 3) as string[];
|
||||
|
||||
store.dispatch(pushReactions(reactions));
|
||||
|
||||
_handleReceivedMessage(store, {
|
||||
participantId: participant.getId(),
|
||||
message: getReactionMessageFromBuffer(data.reactions),
|
||||
message: getReactionMessageFromBuffer(reactions),
|
||||
privateMessage: false,
|
||||
lobbyChat: false,
|
||||
timestamp: data.timestamp
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { IStore } from '../app/types';
|
||||
import { configureInitialDevices, getAvailableDevices } from '../base/devices/actions.web';
|
||||
import { openDialog } from '../base/dialog/actions';
|
||||
import { getJitsiMeetGlobalNSConnectionTimes } from '../base/util/helpers';
|
||||
import { getBackendSafeRoomName } from '../base/util/uri';
|
||||
|
||||
import { DISMISS_CALENDAR_NOTIFICATION } from './actionTypes';
|
||||
@@ -37,29 +38,51 @@ export function dismissCalendarNotification() {
|
||||
/**
|
||||
* Setups initial devices. Makes sure we populate availableDevices list before configuring.
|
||||
*
|
||||
* @param {boolean} recordTimeMetrics - If true, an analytics time metrics will be sent.
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
export function setupInitialDevices() {
|
||||
export function setupInitialDevices(recordTimeMetrics = false) {
|
||||
return async (dispatch: IStore['dispatch']) => {
|
||||
if (recordTimeMetrics) {
|
||||
getJitsiMeetGlobalNSConnectionTimes()['setupInitialDevices.start'] = window.performance.now();
|
||||
}
|
||||
|
||||
await dispatch(getAvailableDevices());
|
||||
|
||||
if (recordTimeMetrics) {
|
||||
getJitsiMeetGlobalNSConnectionTimes()['setupInitialDevices.getAD.finished'] = window.performance.now();
|
||||
}
|
||||
|
||||
await dispatch(configureInitialDevices());
|
||||
|
||||
const now = window.performance.now();
|
||||
|
||||
if (recordTimeMetrics) {
|
||||
getJitsiMeetGlobalNSConnectionTimes()['setupInitialDevices.end'] = now;
|
||||
}
|
||||
logger.debug(`(TIME) setupInitialDevices finished: ${now}`);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Init.
|
||||
*
|
||||
* @param {boolean} shouldDispatchConnect - Whether or not connect should be dispatched. This should be false only when
|
||||
* prejoin is enabled.
|
||||
* @returns {Promise<JitsiConnection>}
|
||||
*/
|
||||
export function init() {
|
||||
export function init(shouldDispatchConnect: boolean) {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
logger.debug(`(TIME) init action dispatched: ${window.performance.now()}`);
|
||||
|
||||
const room = getBackendSafeRoomName(getState()['features/base/conference'].room);
|
||||
|
||||
// XXX For web based version we use conference initialization logic
|
||||
// from the old app (at the moment of writing).
|
||||
return dispatch(setupInitialDevices()).then(
|
||||
return dispatch(setupInitialDevices(true)).then(
|
||||
() => APP.conference.init({
|
||||
roomName: room
|
||||
roomName: room,
|
||||
shouldDispatchConnect
|
||||
}).catch((error: Error) => {
|
||||
APP.API.notifyConferenceLeft(APP.conference.roomName);
|
||||
logger.error(error);
|
||||
|
||||
@@ -104,12 +104,24 @@ interface IProps extends AbstractProps, WithTranslation {
|
||||
|
||||
/**
|
||||
* If visitors queue page is visible or not.
|
||||
* NOTE: This should be set to true once we received an error on connect. Before the first connect this will always
|
||||
* be false.
|
||||
*/
|
||||
_showVisitorsQueue: boolean;
|
||||
|
||||
dispatch: IStore['dispatch'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the prejoin screen should be displayed and false otherwise.
|
||||
*
|
||||
* @param {IProps} props - The props object.
|
||||
* @returns {boolean} - True if the prejoin screen should be displayed and false otherwise.
|
||||
*/
|
||||
function shouldShowPrejoin({ _showPrejoin, _showVisitorsQueue }: IProps) {
|
||||
return _showPrejoin && !_showVisitorsQueue;
|
||||
}
|
||||
|
||||
/**
|
||||
* The conference page of the Web application.
|
||||
*/
|
||||
@@ -265,7 +277,7 @@ class Conference extends AbstractConference<IProps, any> {
|
||||
|
||||
<CalleeInfoContainer />
|
||||
|
||||
{ (_showPrejoin && !_showVisitorsQueue) && <Prejoin />}
|
||||
{ shouldShowPrejoin(this.props) && <Prejoin />}
|
||||
{ (_showLobby && !_showVisitorsQueue) && <LobbyScreen />}
|
||||
{ _showVisitorsQueue && <VisitorsQueue />}
|
||||
</div>
|
||||
@@ -384,7 +396,9 @@ class Conference extends AbstractConference<IProps, any> {
|
||||
|
||||
const { dispatch, t } = this.props;
|
||||
|
||||
dispatch(init());
|
||||
// if we will be showing prejoin we don't want to call connect from init.
|
||||
// Connect will be dispatched from prejoin screen.
|
||||
dispatch(init(!shouldShowPrejoin(this.props)));
|
||||
|
||||
maybeShowSuboptimalExperienceNotification(dispatch, t);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,8 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
didPageUrl,
|
||||
inviteDomain,
|
||||
labels,
|
||||
sharedVideoAllowedURLDomains
|
||||
sharedVideoAllowedURLDomains,
|
||||
supportUrl
|
||||
} = action.value;
|
||||
|
||||
action.value = {
|
||||
@@ -36,7 +37,8 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
didPageUrl,
|
||||
inviteDomain,
|
||||
labels,
|
||||
sharedVideoAllowedURLDomains
|
||||
sharedVideoAllowedURLDomains,
|
||||
supportUrl
|
||||
};
|
||||
|
||||
// The backend may send an empty string, make sure we skip that.
|
||||
|
||||
@@ -159,6 +159,7 @@ export interface IDynamicBrandingState {
|
||||
premeetingBackground: string;
|
||||
sharedVideoAllowedURLDomains?: Array<string>;
|
||||
showGiphyIntegration?: boolean;
|
||||
supportUrl?: string;
|
||||
useDynamicBrandingData: boolean;
|
||||
virtualBackgrounds: Array<Image>;
|
||||
}
|
||||
@@ -184,6 +185,7 @@ ReducerRegistry.register<IDynamicBrandingState>(STORE_NAME, (state = DEFAULT_STA
|
||||
premeetingBackground,
|
||||
sharedVideoAllowedURLDomains,
|
||||
showGiphyIntegration,
|
||||
supportUrl,
|
||||
virtualBackgrounds
|
||||
} = action.value;
|
||||
|
||||
@@ -202,6 +204,7 @@ ReducerRegistry.register<IDynamicBrandingState>(STORE_NAME, (state = DEFAULT_STA
|
||||
premeetingBackground,
|
||||
sharedVideoAllowedURLDomains,
|
||||
showGiphyIntegration,
|
||||
supportUrl,
|
||||
customizationFailed: false,
|
||||
customizationReady: true,
|
||||
useDynamicBrandingData: true,
|
||||
|
||||
@@ -61,3 +61,13 @@ StateListenerRegistry.register(
|
||||
APP.API.notifyOnStageParticipantChanged(participantId);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Updates the on audio only value.
|
||||
*/
|
||||
StateListenerRegistry.register(
|
||||
/* selector */ state => state['features/base/audio-only'].enabled,
|
||||
/* listener */ enabled => {
|
||||
APP.API.notifyAudioOnlyChanged(enabled);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Theme } from '@mui/material';
|
||||
import React, { isValidElement, useCallback, useContext } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { keyframes } from 'tss-react';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
@@ -15,6 +16,7 @@ import {
|
||||
IconWarningCircle
|
||||
} from '../../../base/icons/svg';
|
||||
import Message from '../../../base/react/components/web/Message';
|
||||
import { getSupportUrl } from '../../../base/react/functions';
|
||||
import { withPixelLineHeight } from '../../../base/styles/functions.web';
|
||||
import { NOTIFICATION_ICON, NOTIFICATION_TYPE } from '../../constants';
|
||||
import { INotificationProps } from '../../types';
|
||||
@@ -190,6 +192,7 @@ const Notification = ({
|
||||
const { classes, cx, theme } = useStyles();
|
||||
const { t } = useTranslation();
|
||||
const { unmounting } = useContext(NotificationsTransitionContext);
|
||||
const supportUrl = useSelector(getSupportUrl);
|
||||
|
||||
const ICON_COLOR = {
|
||||
error: theme.palette.iconError,
|
||||
@@ -229,9 +232,9 @@ const Notification = ({
|
||||
);
|
||||
}, [ description, descriptionArguments, descriptionKey, classes ]);
|
||||
|
||||
const _onOpenSupportLink = () => {
|
||||
window.open(interfaceConfig.SUPPORT_URL, '_blank', 'noopener');
|
||||
};
|
||||
const _onOpenSupportLink = useCallback(() => {
|
||||
window.open(supportUrl, '_blank', 'noopener');
|
||||
}, [ supportUrl ]);
|
||||
|
||||
const mapAppearanceToButtons = useCallback((): {
|
||||
content: string; onClick: () => void; testId?: string; type?: string; }[] => {
|
||||
@@ -244,7 +247,7 @@ const Notification = ({
|
||||
}
|
||||
];
|
||||
|
||||
if (!hideErrorSupportLink && interfaceConfig.SUPPORT_URL) {
|
||||
if (!hideErrorSupportLink && supportUrl) {
|
||||
buttons.push({
|
||||
content: t('dialog.contactSupport'),
|
||||
onClick: _onOpenSupportLink
|
||||
@@ -279,7 +282,7 @@ const Notification = ({
|
||||
|
||||
return [];
|
||||
}
|
||||
}, [ appearance, onDismiss, customActionHandler, customActionNameKey, hideErrorSupportLink ]);
|
||||
}, [ appearance, onDismiss, customActionHandler, customActionNameKey, hideErrorSupportLink, supportUrl ]);
|
||||
|
||||
const getIcon = useCallback(() => {
|
||||
let iconToDisplay;
|
||||
@@ -313,7 +316,7 @@ const Notification = ({
|
||||
<div
|
||||
aria-atomic = 'false'
|
||||
aria-live = 'polite'
|
||||
className = { cx(classes.container, unmounting.get(uid ?? '') && 'unmount') }
|
||||
className = { cx(classes.container, (unmounting.get(uid ?? '') && 'unmount') as string | undefined) }
|
||||
data-testid = { titleKey || descriptionKey }
|
||||
id = { uid }>
|
||||
<div className = { cx(classes.ribbon, appearance) } />
|
||||
|
||||
@@ -9,6 +9,17 @@
|
||||
*/
|
||||
export const REMOVE_TRANSCRIPT_MESSAGE = 'REMOVE_TRANSCRIPT_MESSAGE';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which indicates that an cached transcript
|
||||
* has to be removed from the state.
|
||||
*
|
||||
* {
|
||||
* type: REMOVE_CACHED_TRANSCRIPT_MESSAGE,
|
||||
* transciptMessageID: string,
|
||||
* }
|
||||
*/
|
||||
export const REMOVE_CACHED_TRANSCRIPT_MESSAGE = 'REMOVE_CACHED_TRANSCRIPT_MESSAGE';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which indicates that a transcript with an
|
||||
* given message_id to be added or updated is received.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { DEFAULT_LANGUAGE } from '../base/i18n/i18next';
|
||||
|
||||
import {
|
||||
REMOVE_CACHED_TRANSCRIPT_MESSAGE,
|
||||
REMOVE_TRANSCRIPT_MESSAGE,
|
||||
SET_REQUESTING_SUBTITLES,
|
||||
TOGGLE_REQUESTING_SUBTITLES,
|
||||
@@ -23,6 +24,22 @@ export function removeTranscriptMessage(transcriptMessageID: string) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals that a cached transcript has to be removed from the state.
|
||||
*
|
||||
* @param {string} transcriptMessageID - The message_id to be removed.
|
||||
* @returns {{
|
||||
* type: REMOVE_CACHED_TRANSCRIPT_MESSAGE,
|
||||
* transcriptMessageID: string,
|
||||
* }}
|
||||
*/
|
||||
export function removeCachedTranscriptMessage(transcriptMessageID: string) {
|
||||
return {
|
||||
type: REMOVE_CACHED_TRANSCRIPT_MESSAGE,
|
||||
transcriptMessageID
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals that a transcript with the given message_id to be added or updated
|
||||
* is received.
|
||||
|
||||
@@ -6,12 +6,14 @@ import { isJwtFeatureEnabled } from '../base/jwt/functions';
|
||||
import JitsiMeetJS from '../base/lib-jitsi-meet';
|
||||
import { isLocalParticipantModerator } from '../base/participants/functions';
|
||||
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
|
||||
import { TRANSCRIBER_JOINED } from '../transcribing/actionTypes';
|
||||
|
||||
import {
|
||||
SET_REQUESTING_SUBTITLES,
|
||||
TOGGLE_REQUESTING_SUBTITLES
|
||||
} from './actionTypes';
|
||||
import {
|
||||
removeCachedTranscriptMessage,
|
||||
removeTranscriptMessage,
|
||||
setRequestingSubtitles,
|
||||
updateTranscriptMessage
|
||||
@@ -80,6 +82,15 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
_requestingSubtitlesChange(store, toggledValue, state._language);
|
||||
break;
|
||||
}
|
||||
case TRANSCRIBER_JOINED: {
|
||||
const { transcription } = store.getState()['features/base/config'];
|
||||
|
||||
if (transcription?.autoCaptionOnTranscribe) {
|
||||
store.dispatch(setRequestingSubtitles(true));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case SET_REQUESTING_SUBTITLES:
|
||||
_requestingSubtitlesChange(store, action.enabled, action.language);
|
||||
break;
|
||||
@@ -124,18 +135,16 @@ function _endpointMessageReceived(store: IStore, next: Function, action: AnyActi
|
||||
name
|
||||
};
|
||||
|
||||
let newTranscriptMessage: ITranscriptMessage | undefined;
|
||||
|
||||
if (json.type === JSON_TYPE_TRANSLATION_RESULT && json.language === language) {
|
||||
// Displays final results in the target language if translation is
|
||||
// enabled.
|
||||
|
||||
const newTranscriptMessage = {
|
||||
newTranscriptMessage = {
|
||||
clearTimeOut: undefined,
|
||||
final: json.text,
|
||||
final: json.text?.trim(),
|
||||
participant
|
||||
};
|
||||
|
||||
_setClearerOnTranscriptMessage(dispatch, transcriptMessageID, newTranscriptMessage);
|
||||
dispatch(updateTranscriptMessage(transcriptMessageID, newTranscriptMessage));
|
||||
} else if (json.type === JSON_TYPE_TRANSCRIPTION_RESULT) {
|
||||
// Displays interim and final results without any translation if
|
||||
// translations are disabled.
|
||||
@@ -199,13 +208,12 @@ function _endpointMessageReceived(store: IStore, next: Function, action: AnyActi
|
||||
// message ID or adds a new transcript message if it does not
|
||||
// exist in the map.
|
||||
const existingMessage = state['features/subtitles']._transcriptMessages.get(transcriptMessageID);
|
||||
const newTranscriptMessage: ITranscriptMessage = {
|
||||
|
||||
newTranscriptMessage = {
|
||||
clearTimeOut: existingMessage?.clearTimeOut,
|
||||
participant
|
||||
};
|
||||
|
||||
_setClearerOnTranscriptMessage(dispatch, transcriptMessageID, newTranscriptMessage);
|
||||
|
||||
// If this is final result, update the state as a final result
|
||||
// and start a count down to remove the subtitle from the state
|
||||
if (!json.is_interim) {
|
||||
@@ -221,7 +229,31 @@ function _endpointMessageReceived(store: IStore, next: Function, action: AnyActi
|
||||
// after the stable part.
|
||||
newTranscriptMessage.unstable = text;
|
||||
}
|
||||
}
|
||||
|
||||
if (newTranscriptMessage) {
|
||||
if (newTranscriptMessage.final) {
|
||||
const cachedTranscriptMessage
|
||||
= state['features/subtitles']._cachedTranscriptMessages?.get(transcriptMessageID);
|
||||
|
||||
if (cachedTranscriptMessage) {
|
||||
const cachedText = (cachedTranscriptMessage.stable || cachedTranscriptMessage.unstable)?.trim();
|
||||
const newText = newTranscriptMessage.final;
|
||||
|
||||
if (cachedText && cachedText.length > 0 && newText && newText.length > 0
|
||||
&& newText.toLowerCase().startsWith(cachedText.toLowerCase())) {
|
||||
newTranscriptMessage.final = newText.slice(cachedText.length)?.trim();
|
||||
}
|
||||
dispatch(removeCachedTranscriptMessage(transcriptMessageID));
|
||||
|
||||
if (!newTranscriptMessage.final || newTranscriptMessage.final.length === 0) {
|
||||
return next(action);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
_setClearerOnTranscriptMessage(dispatch, transcriptMessageID, newTranscriptMessage);
|
||||
dispatch(updateTranscriptMessage(transcriptMessageID, newTranscriptMessage));
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import ReducerRegistry from '../base/redux/ReducerRegistry';
|
||||
import { TRANSCRIBER_LEFT } from '../transcribing/actionTypes';
|
||||
|
||||
import {
|
||||
REMOVE_CACHED_TRANSCRIPT_MESSAGE,
|
||||
REMOVE_TRANSCRIPT_MESSAGE,
|
||||
SET_REQUESTING_SUBTITLES,
|
||||
TOGGLE_REQUESTING_SUBTITLES,
|
||||
@@ -13,6 +14,7 @@ import { ITranscriptMessage } from './types';
|
||||
* Default State for 'features/transcription' feature.
|
||||
*/
|
||||
const defaultState = {
|
||||
_cachedTranscriptMessages: new Map(),
|
||||
_displaySubtitles: false,
|
||||
_transcriptMessages: new Map(),
|
||||
_requestingSubtitles: false,
|
||||
@@ -20,6 +22,7 @@ const defaultState = {
|
||||
};
|
||||
|
||||
export interface ISubtitlesState {
|
||||
_cachedTranscriptMessages: Map<string, ITranscriptMessage>;
|
||||
_displaySubtitles: boolean;
|
||||
_language: string | null;
|
||||
_requestingSubtitles: boolean;
|
||||
@@ -35,6 +38,8 @@ ReducerRegistry.register<ISubtitlesState>('features/subtitles', (
|
||||
switch (action.type) {
|
||||
case REMOVE_TRANSCRIPT_MESSAGE:
|
||||
return _removeTranscriptMessage(state, action);
|
||||
case REMOVE_CACHED_TRANSCRIPT_MESSAGE:
|
||||
return _removeCachedTranscriptMessage(state, action);
|
||||
case UPDATE_TRANSCRIPT_MESSAGE:
|
||||
return _updateTranscriptMessage(state, action);
|
||||
case SET_REQUESTING_SUBTITLES:
|
||||
@@ -70,16 +75,45 @@ ReducerRegistry.register<ISubtitlesState>('features/subtitles', (
|
||||
*/
|
||||
function _removeTranscriptMessage(state: ISubtitlesState, { transcriptMessageID }: { transcriptMessageID: string; }) {
|
||||
const newTranscriptMessages = new Map(state._transcriptMessages);
|
||||
const message = newTranscriptMessages.get(transcriptMessageID);
|
||||
let { _cachedTranscriptMessages } = state;
|
||||
|
||||
if (message && !message.final) {
|
||||
_cachedTranscriptMessages = new Map(_cachedTranscriptMessages);
|
||||
_cachedTranscriptMessages.set(transcriptMessageID, message);
|
||||
}
|
||||
|
||||
// Deletes the key from Map once a final message arrives.
|
||||
newTranscriptMessages.delete(transcriptMessageID);
|
||||
|
||||
return {
|
||||
...state,
|
||||
_cachedTranscriptMessages,
|
||||
_transcriptMessages: newTranscriptMessages
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reduces a specific Redux action REMOVE_CACHED_TRANSCRIPT_MESSAGE of the feature transcription.
|
||||
*
|
||||
* @param {Object} state - The Redux state of the feature transcription.
|
||||
* @param {Action} action -The Redux action REMOVE_CACHED_TRANSCRIPT_MESSAGE to reduce.
|
||||
* @returns {Object} The new state of the feature transcription after the reduction of the specified action.
|
||||
*/
|
||||
function _removeCachedTranscriptMessage(state: ISubtitlesState,
|
||||
{ transcriptMessageID }: { transcriptMessageID: string; }) {
|
||||
const newCachedTranscriptMessages = new Map(state._cachedTranscriptMessages);
|
||||
|
||||
// Deletes the key from Map once a final message arrives.
|
||||
newCachedTranscriptMessages.delete(transcriptMessageID);
|
||||
|
||||
return {
|
||||
...state,
|
||||
_cachedTranscriptMessages: newCachedTranscriptMessages
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduces a specific Redux action UPDATE_TRANSCRIPT_MESSAGE of the feature
|
||||
* transcription.
|
||||
@@ -92,12 +126,16 @@ function _removeTranscriptMessage(state: ISubtitlesState, { transcriptMessageID
|
||||
function _updateTranscriptMessage(state: ISubtitlesState, { transcriptMessageID, newTranscriptMessage }:
|
||||
{ newTranscriptMessage: ITranscriptMessage; transcriptMessageID: string; }) {
|
||||
const newTranscriptMessages = new Map(state._transcriptMessages);
|
||||
const _cachedTranscriptMessages = new Map(state._cachedTranscriptMessages);
|
||||
|
||||
_cachedTranscriptMessages.delete(transcriptMessageID);
|
||||
|
||||
// Updates the new message for the given key in the Map.
|
||||
newTranscriptMessages.set(transcriptMessageID, newTranscriptMessage);
|
||||
|
||||
return {
|
||||
...state,
|
||||
_cachedTranscriptMessages,
|
||||
_transcriptMessages: newTranscriptMessages
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { showErrorNotification } from '../notifications/actions';
|
||||
import { NOTIFICATION_TIMEOUT_TYPE } from '../notifications/constants';
|
||||
|
||||
import { TRANSCRIBER_LEFT } from './actionTypes';
|
||||
import './subscriber';
|
||||
|
||||
/**
|
||||
* Implements the middleware of the feature transcribing.
|
||||
|
||||
@@ -12,7 +12,7 @@ import { isLiveStreamingRunning, isRecordingRunning } from '../recording/functio
|
||||
import { isRecorderTranscriptionsRunning } from './functions';
|
||||
|
||||
/**
|
||||
* Listens for large video participant ID changes.
|
||||
* Listens for transcriber status change.
|
||||
*/
|
||||
StateListenerRegistry.register(
|
||||
/* selector */ isRecorderTranscriptionsRunning,
|
||||
|
||||
@@ -194,7 +194,7 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
delay = msg.randomDelayMs;
|
||||
}
|
||||
|
||||
if (WebsocketClient.getInstance().connectCount > 1) {
|
||||
if (WebsocketClient.getInstance().connectCount > 3) {
|
||||
// if we keep connecting/disconnecting, let's slow it down
|
||||
delay = 30 * 1000;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import ReactDOM from 'react-dom';
|
||||
import { App } from './features/app/components/App.web';
|
||||
import { getLogger } from './features/base/logging/functions';
|
||||
import Platform from './features/base/react/Platform.web';
|
||||
import { getJitsiMeetGlobalNS } from './features/base/util/helpers';
|
||||
import { getJitsiMeetGlobalNS, getJitsiMeetGlobalNSConnectionTimes } from './features/base/util/helpers';
|
||||
import DialInSummaryApp from './features/invite/components/dial-in-summary/web/DialInSummaryApp';
|
||||
import PrejoinApp from './features/prejoin/components/web/PrejoinApp';
|
||||
import WhiteboardApp from './features/whiteboard/components/web/WhiteboardApp';
|
||||
@@ -45,20 +45,19 @@ if (Platform.OS === 'ios') {
|
||||
}
|
||||
|
||||
const globalNS = getJitsiMeetGlobalNS();
|
||||
const connectionTimes = getJitsiMeetGlobalNSConnectionTimes();
|
||||
|
||||
// Used for automated performance tests.
|
||||
globalNS.connectionTimes = {
|
||||
'index.loaded': window.indexLoadedTime
|
||||
};
|
||||
connectionTimes['index.loaded'] = window.indexLoadedTime;
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
globalNS.connectionTimes['window.loaded'] = window.loadedEventTime;
|
||||
connectionTimes['window.loaded'] = window.loadedEventTime;
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const now = window.performance.now();
|
||||
|
||||
globalNS.connectionTimes['document.ready'] = now;
|
||||
connectionTimes['document.ready'] = now;
|
||||
logger.log('(TIME) document ready:\t', now);
|
||||
});
|
||||
|
||||
|
||||
@@ -353,15 +353,15 @@ module:hook('muc-broadcast-presence', function (event)
|
||||
module:send(promotion_request);
|
||||
end
|
||||
|
||||
local requestTranscriptionValue = full_p:get_child_text(PARTICIPANT_PROP_REQUEST_TRANSCRIPTION);
|
||||
local hasTranscriptionEnabled = room._transcription_languages and room._transcription_languages[occupant.jid];
|
||||
|
||||
-- detect transcription
|
||||
if full_p:get_child_text(PARTICIPANT_PROP_REQUEST_TRANSCRIPTION) then
|
||||
if requestTranscriptionValue == 'true' then
|
||||
local lang = full_p:get_child_text(PARTICIPANT_PROP_TRANSLATION_LANG);
|
||||
|
||||
occupant._transcription_enabled = true;
|
||||
|
||||
add_transcription(room, occupant, lang);
|
||||
elseif occupant._transcription_enabled then
|
||||
occupant._transcription_enabled = false;
|
||||
elseif hasTranscriptionEnabled then
|
||||
remove_transcription(room, occupant, nil);
|
||||
end
|
||||
|
||||
|
||||
@@ -113,6 +113,11 @@ module:hook("message/bare", function(event)
|
||||
return
|
||||
end
|
||||
|
||||
if room.polls.by_id[data.pollId] ~= nil then
|
||||
module:log("error", "Poll already exists: %s", data.pollId);
|
||||
return;
|
||||
end
|
||||
|
||||
local answers = {}
|
||||
local compact_answers = {}
|
||||
for i, name in ipairs(data.answers) do
|
||||
|
||||
@@ -377,7 +377,7 @@ local function go_live(room)
|
||||
|
||||
-- if missing we assume room is live, only skip if it is marked explicitly as false
|
||||
if room.jitsiMetadata and room.jitsiMetadata.visitors
|
||||
and room.jitsiMetadata.visitors.live and room.jitsiMetadata.visitors.live == false then
|
||||
and room.jitsiMetadata.visitors.live ~= nil and room.jitsiMetadata.visitors.live == false then
|
||||
return;
|
||||
end
|
||||
|
||||
@@ -600,7 +600,13 @@ process_host_module(muc_domain_prefix..'.'..muc_domain_base, function(host_modul
|
||||
go_live(event.room);
|
||||
end);
|
||||
host_module:hook('muc-occupant-joined', function (event)
|
||||
go_live(event.room);
|
||||
local room = event.room;
|
||||
|
||||
if is_healthcheck_room(room.jid) then
|
||||
return;
|
||||
end
|
||||
|
||||
go_live(room);
|
||||
end);
|
||||
end
|
||||
|
||||
|
||||
Reference in New Issue
Block a user