2020-06-18 19:15:49 -04:00
|
|
|
/* global APP, JitsiMeetJS, config, interfaceConfig */
|
2016-11-11 09:00:54 -06:00
|
|
|
|
2021-04-07 10:03:20 -05:00
|
|
|
import { jitsiLocalStorage } from '@jitsi/js-utils';
|
2021-11-10 11:11:29 +01:00
|
|
|
import Logger from '@jitsi/logger';
|
2016-10-11 19:08:24 -05:00
|
|
|
|
2020-03-20 13:51:26 +02:00
|
|
|
import { ENDPOINT_TEXT_MESSAGE_NAME } from './modules/API/constants';
|
2020-05-20 12:57:03 +02:00
|
|
|
import mediaDeviceHelper from './modules/devices/mediaDeviceHelper';
|
|
|
|
|
import Recorder from './modules/recorder/Recorder';
|
2018-08-31 13:02:04 -07:00
|
|
|
import { createTaskQueue } from './modules/util/helpers';
|
2017-12-11 10:48:32 -08:00
|
|
|
import {
|
2018-01-03 15:24:07 -06:00
|
|
|
createDeviceChangedEvent,
|
|
|
|
|
createScreenSharingEvent,
|
2022-09-27 10:10:28 +03:00
|
|
|
createStartSilentEvent,
|
2023-04-03 13:49:19 +03:00
|
|
|
createTrackMutedEvent
|
|
|
|
|
} from './react/features/analytics/AnalyticsEvents';
|
|
|
|
|
import { sendAnalytics } from './react/features/analytics/functions';
|
2018-02-26 16:50:27 -06:00
|
|
|
import {
|
2019-07-01 13:02:25 +01:00
|
|
|
maybeRedirectToWelcomePage,
|
2018-02-26 16:50:27 -06:00
|
|
|
reloadWithStoredParams
|
2020-06-04 16:09:13 +02:00
|
|
|
} from './react/features/app/actions';
|
2017-02-27 15:42:28 -06:00
|
|
|
import {
|
2021-09-14 17:31:30 +02:00
|
|
|
_conferenceWillJoin,
|
2018-06-20 15:19:53 -05:00
|
|
|
authStatusChanged,
|
2017-02-27 15:42:28 -06:00
|
|
|
conferenceFailed,
|
2022-04-05 21:13:39 -05:00
|
|
|
conferenceJoinInProgress,
|
2022-09-27 10:10:28 +03:00
|
|
|
conferenceJoined,
|
2017-02-28 17:12:02 -06:00
|
|
|
conferenceLeft,
|
2024-10-28 12:24:42 -05:00
|
|
|
conferencePropertiesChanged,
|
2019-03-12 17:45:53 +00:00
|
|
|
conferenceSubjectChanged,
|
2020-01-13 19:12:25 +02:00
|
|
|
conferenceTimestampChanged,
|
2021-02-03 12:28:39 +02:00
|
|
|
conferenceUniqueIdSet,
|
2023-07-15 17:33:26 -05:00
|
|
|
conferenceWillInit,
|
2018-08-01 11:41:54 -05:00
|
|
|
conferenceWillLeave,
|
2022-11-15 16:54:24 +01:00
|
|
|
dataChannelClosed,
|
2017-08-09 12:40:03 -07:00
|
|
|
dataChannelOpened,
|
2022-04-01 13:50:52 +01:00
|
|
|
e2eRttChanged,
|
2024-02-05 10:30:21 +01:00
|
|
|
endpointMessageReceived,
|
2019-06-26 14:53:48 -07:00
|
|
|
kickedOut,
|
2017-08-09 12:40:03 -07:00
|
|
|
lockStateChanged,
|
2022-09-27 10:10:28 +03:00
|
|
|
nonParticipantMessageReceived,
|
2017-11-20 18:21:35 -08:00
|
|
|
onStartMutedPolicyChanged,
|
2023-03-31 14:04:33 +03:00
|
|
|
p2pStatusChanged
|
|
|
|
|
} from './react/features/base/conference/actions';
|
|
|
|
|
import {
|
|
|
|
|
AVATAR_URL_COMMAND,
|
|
|
|
|
CONFERENCE_LEAVE_REASONS,
|
|
|
|
|
EMAIL_COMMAND
|
|
|
|
|
} from './react/features/base/conference/constants';
|
|
|
|
|
import {
|
|
|
|
|
commonUserJoinedHandling,
|
|
|
|
|
commonUserLeftHandling,
|
|
|
|
|
getConferenceOptions,
|
2025-07-21 16:55:22 +03:00
|
|
|
sendLocalParticipant,
|
|
|
|
|
updateTrackMuteState
|
2023-03-31 14:04:33 +03:00
|
|
|
} from './react/features/base/conference/functions';
|
2024-04-29 14:05:59 -04:00
|
|
|
import { getReplaceParticipant, getSsrcRewritingFeatureFlag } from './react/features/base/config/functions';
|
2023-07-15 17:33:26 -05:00
|
|
|
import { connect } from './react/features/base/connection/actions.web';
|
2018-04-12 21:58:20 +02:00
|
|
|
import {
|
2019-05-03 18:25:33 +01:00
|
|
|
checkAndNotifyForNewDevice,
|
2018-08-06 08:24:59 -07:00
|
|
|
getAvailableDevices,
|
2019-05-29 14:17:07 -07:00
|
|
|
notifyCameraError,
|
|
|
|
|
notifyMicError,
|
2018-04-12 21:58:20 +02:00
|
|
|
updateDeviceList
|
2022-11-01 13:36:32 +01:00
|
|
|
} from './react/features/base/devices/actions.web';
|
|
|
|
|
import {
|
2023-09-21 20:06:55 -05:00
|
|
|
areDevicesDifferent,
|
|
|
|
|
filterIgnoredDevices,
|
|
|
|
|
flattenAvailableDevices,
|
2022-11-01 13:36:32 +01:00
|
|
|
getDefaultDeviceId,
|
2023-09-21 20:06:55 -05:00
|
|
|
logDevices,
|
2022-11-01 13:36:32 +01:00
|
|
|
setAudioOutputDeviceId
|
|
|
|
|
} from './react/features/base/devices/functions.web';
|
2017-02-18 18:42:11 -06:00
|
|
|
import {
|
2017-10-10 18:31:40 -05:00
|
|
|
JitsiConferenceErrors,
|
|
|
|
|
JitsiConferenceEvents,
|
2022-04-01 13:50:52 +01:00
|
|
|
JitsiE2ePingEvents,
|
2017-10-10 18:31:40 -05:00
|
|
|
JitsiMediaDevicesEvents,
|
2022-09-27 10:10:28 +03:00
|
|
|
JitsiTrackEvents,
|
|
|
|
|
browser
|
2017-02-18 18:42:11 -06:00
|
|
|
} from './react/features/base/lib-jitsi-meet';
|
2017-07-24 15:56:57 +02:00
|
|
|
import {
|
2023-05-18 14:16:37 -05:00
|
|
|
gumPending,
|
2017-07-24 15:56:57 +02:00
|
|
|
setAudioAvailable,
|
2017-08-18 12:30:30 +01:00
|
|
|
setAudioMuted,
|
2021-11-30 15:08:25 -05:00
|
|
|
setAudioUnmutePermissions,
|
2024-02-12 18:35:51 -06:00
|
|
|
setInitialGUMPromise,
|
2017-08-18 12:30:30 +01:00
|
|
|
setVideoAvailable,
|
2021-11-30 15:08:25 -05:00
|
|
|
setVideoMuted,
|
|
|
|
|
setVideoUnmutePermissions
|
2023-04-03 13:49:19 +03:00
|
|
|
} from './react/features/base/media/actions';
|
2025-06-09 23:44:24 +03:00
|
|
|
import { MEDIA_TYPE, VIDEO_MUTISM_AUTHORITY, VIDEO_TYPE } from './react/features/base/media/constants';
|
2023-04-03 13:49:19 +03:00
|
|
|
import {
|
|
|
|
|
getStartWithAudioMuted,
|
|
|
|
|
getStartWithVideoMuted,
|
|
|
|
|
isVideoMutedByUser
|
|
|
|
|
} from './react/features/base/media/functions';
|
2023-05-18 14:16:37 -05:00
|
|
|
import { IGUMPendingState } from './react/features/base/media/types';
|
2017-02-27 15:42:28 -06:00
|
|
|
import {
|
2017-09-18 11:35:52 -07:00
|
|
|
dominantSpeakerChanged,
|
2022-01-21 10:07:55 +02:00
|
|
|
localParticipantAudioLevelChanged,
|
2017-04-10 14:53:30 -07:00
|
|
|
localParticipantRoleChanged,
|
2019-06-17 16:00:09 +02:00
|
|
|
participantKicked,
|
|
|
|
|
participantMutedUs,
|
2017-07-31 16:33:22 -07:00
|
|
|
participantPresenceChanged,
|
2017-03-23 13:01:33 -05:00
|
|
|
participantRoleChanged,
|
2023-01-24 13:58:58 -05:00
|
|
|
participantSourcesUpdated,
|
2020-11-13 22:09:25 -06:00
|
|
|
participantUpdated,
|
2022-04-14 13:07:17 -04:00
|
|
|
screenshareParticipantDisplayNameChanged,
|
2020-11-13 22:09:25 -06:00
|
|
|
updateRemoteParticipantFeatures
|
2023-04-03 13:49:19 +03:00
|
|
|
} from './react/features/base/participants/actions';
|
|
|
|
|
import {
|
|
|
|
|
getLocalParticipant,
|
|
|
|
|
getNormalizedDisplayName,
|
2024-08-26 08:47:31 -05:00
|
|
|
getParticipantByIdOrUndefined,
|
2023-04-03 13:49:19 +03:00
|
|
|
getVirtualScreenshareParticipantByOwnerId
|
|
|
|
|
} from './react/features/base/participants/functions';
|
|
|
|
|
import { updateSettings } from './react/features/base/settings/actions';
|
2019-11-26 05:57:03 -05:00
|
|
|
import {
|
2022-03-15 13:24:49 -04:00
|
|
|
addLocalTrack,
|
2024-05-07 15:32:45 -05:00
|
|
|
createInitialAVTracks,
|
2019-01-01 13:19:34 -08:00
|
|
|
destroyLocalTracks,
|
2024-05-07 15:32:45 -05:00
|
|
|
displayErrorsForCreateInitialLocalTracks,
|
2023-04-03 13:49:19 +03:00
|
|
|
replaceLocalTrack,
|
2024-05-07 15:32:45 -05:00
|
|
|
setGUMPendingStateOnFailedTracks,
|
2023-04-03 13:49:19 +03:00
|
|
|
toggleScreensharing as toggleScreensharingA,
|
|
|
|
|
trackAdded,
|
|
|
|
|
trackRemoved
|
|
|
|
|
} from './react/features/base/tracks/actions';
|
|
|
|
|
import {
|
|
|
|
|
createLocalTracksF,
|
2020-06-26 11:54:12 +03:00
|
|
|
getLocalJitsiAudioTrack,
|
|
|
|
|
getLocalJitsiVideoTrack,
|
2021-10-14 13:17:56 +03:00
|
|
|
getLocalVideoTrack,
|
2017-08-18 12:30:30 +01:00
|
|
|
isLocalTrackMuted,
|
2023-04-03 13:49:19 +03:00
|
|
|
isUserInteractionRequiredForUnmute
|
|
|
|
|
} from './react/features/base/tracks/functions';
|
2025-09-05 22:52:35 +02:00
|
|
|
import { getLocalJitsiAudioTrackSettings } from './react/features/base/tracks/functions.web';
|
2020-10-02 16:20:24 +03:00
|
|
|
import { downloadJSON } from './react/features/base/util/downloadJSON';
|
2024-10-31 12:49:57 -05:00
|
|
|
import { getJitsiMeetGlobalNSConnectionTimes } from './react/features/base/util/helpers';
|
2023-08-28 15:14:03 +03:00
|
|
|
import { openLeaveReasonDialog } from './react/features/conference/actions.web';
|
2023-03-31 14:04:33 +03:00
|
|
|
import { showDesktopPicker } from './react/features/desktop-picker/actions';
|
|
|
|
|
import { appendSuffix } from './react/features/display-name/functions';
|
|
|
|
|
import { maybeOpenFeedbackDialog, submitFeedback } from './react/features/feedback/actions';
|
2022-03-03 20:29:38 +03:00
|
|
|
import { maybeSetLobbyChatMessageListener } from './react/features/lobby/actions.any';
|
2022-07-20 15:31:17 +03:00
|
|
|
import { setNoiseSuppressionEnabled } from './react/features/noise-suppression/actions';
|
2023-07-15 17:33:26 -05:00
|
|
|
import {
|
|
|
|
|
hideNotification,
|
|
|
|
|
showErrorNotification,
|
|
|
|
|
showNotification,
|
|
|
|
|
showWarningNotification
|
|
|
|
|
} from './react/features/notifications/actions';
|
2021-11-24 13:05:27 +02:00
|
|
|
import {
|
2022-11-15 16:54:24 +01:00
|
|
|
DATA_CHANNEL_CLOSED_NOTIFICATION_ID,
|
2023-03-31 14:04:33 +03:00
|
|
|
NOTIFICATION_TIMEOUT_TYPE
|
|
|
|
|
} from './react/features/notifications/constants';
|
|
|
|
|
import { suspendDetected } from './react/features/power-monitor/actions';
|
2024-06-25 12:27:02 +03:00
|
|
|
import { initPrejoin, isPrejoinPageVisible } from './react/features/prejoin/functions';
|
2023-04-03 13:49:19 +03:00
|
|
|
import { disableReceiver, stopReceiver } from './react/features/remote-control/actions';
|
2023-03-31 14:04:33 +03:00
|
|
|
import { setScreenAudioShareState } from './react/features/screen-share/actions.web';
|
|
|
|
|
import { isScreenAudioShared } from './react/features/screen-share/functions';
|
|
|
|
|
import { toggleScreenshotCaptureSummary } from './react/features/screenshot-capture/actions';
|
2025-09-05 22:52:35 +02:00
|
|
|
import { setAudioSettings } from './react/features/settings/actions.web';
|
2020-03-26 14:17:44 +02:00
|
|
|
import { AudioMixerEffect } from './react/features/stream-effects/audio-mixer/AudioMixerEffect';
|
2021-08-04 12:56:07 +02:00
|
|
|
import { createRnnoiseProcessor } from './react/features/stream-effects/rnnoise';
|
2022-08-30 11:42:29 +03:00
|
|
|
import { handleToggleVideoMuted } from './react/features/toolbox/actions.any';
|
2024-10-02 18:59:04 -05:00
|
|
|
import { transcriberJoined, transcriberLeft } from './react/features/transcribing/actions';
|
2022-02-17 16:25:31 -06:00
|
|
|
import { muteLocal } from './react/features/video-menu/actions.any';
|
2017-02-27 15:42:28 -06:00
|
|
|
|
2025-09-09 19:12:01 -04:00
|
|
|
const logger = Logger.getLogger('app:conference-web');
|
2017-04-11 14:40:03 -05:00
|
|
|
let room;
|
2021-09-23 14:54:27 -05:00
|
|
|
|
2017-03-30 09:58:31 -07:00
|
|
|
/*
|
|
|
|
|
* Logic to open a desktop picker put on the window global for
|
2025-02-06 11:41:22 +01:00
|
|
|
* lib-jitsi-meet to detect and invoke.
|
|
|
|
|
*
|
|
|
|
|
* TODO: remove once the Electron SDK supporting gDM has been out for a while.
|
2017-03-30 09:58:31 -07:00
|
|
|
*/
|
|
|
|
|
window.JitsiMeetScreenObtainer = {
|
2017-07-09 16:34:08 -05:00
|
|
|
openDesktopPicker(options, onSourceChoose) {
|
|
|
|
|
APP.store.dispatch(showDesktopPicker(options, onSourceChoose));
|
2017-03-30 09:58:31 -07:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2016-06-13 16:11:44 -05:00
|
|
|
/**
|
|
|
|
|
* Known custom conference commands.
|
|
|
|
|
*/
|
|
|
|
|
const commands = {
|
2017-02-28 17:12:02 -06:00
|
|
|
AVATAR_URL: AVATAR_URL_COMMAND,
|
2017-10-12 18:02:29 -05:00
|
|
|
CUSTOM_ROLE: 'custom-role',
|
2017-02-28 17:12:02 -06:00
|
|
|
EMAIL: EMAIL_COMMAND,
|
2021-04-16 12:43:34 +03:00
|
|
|
ETHERPAD: 'etherpad'
|
2016-06-13 16:11:44 -05:00
|
|
|
};
|
|
|
|
|
|
2016-01-15 16:59:35 +02:00
|
|
|
/**
|
2016-06-13 16:11:44 -05:00
|
|
|
* Share data to other users.
|
|
|
|
|
* @param command the command
|
|
|
|
|
* @param {string} value new value
|
2016-01-15 16:59:35 +02:00
|
|
|
*/
|
2017-04-11 14:40:03 -05:00
|
|
|
function sendData(command, value) {
|
2017-08-18 12:30:30 +01:00
|
|
|
if (!room) {
|
2017-03-23 13:01:33 -05:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2016-06-13 16:11:44 -05:00
|
|
|
room.removeCommand(command);
|
2017-10-12 18:02:29 -05:00
|
|
|
room.sendCommand(command, { value });
|
2016-01-15 16:59:35 +02:00
|
|
|
}
|
2016-01-06 16:39:13 -06:00
|
|
|
|
2018-08-31 13:02:04 -07:00
|
|
|
/**
|
|
|
|
|
* A queue for the async replaceLocalTrack action so that multiple audio
|
|
|
|
|
* replacements cannot happen simultaneously. This solves the issue where
|
|
|
|
|
* replaceLocalTrack is called multiple times with an oldTrack of null, causing
|
|
|
|
|
* multiple local tracks of the same type to be used.
|
|
|
|
|
*
|
|
|
|
|
* @private
|
|
|
|
|
* @type {Object}
|
|
|
|
|
*/
|
|
|
|
|
const _replaceLocalAudioTrackQueue = createTaskQueue();
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* A task queue for replacement local video tracks. This separate queue exists
|
|
|
|
|
* so video replacement is not blocked by audio replacement tasks in the queue
|
|
|
|
|
* {@link _replaceLocalAudioTrackQueue}.
|
|
|
|
|
*
|
|
|
|
|
* @private
|
|
|
|
|
* @type {Object}
|
|
|
|
|
*/
|
|
|
|
|
const _replaceLocalVideoTrackQueue = createTaskQueue();
|
|
|
|
|
|
2017-10-12 18:02:29 -05:00
|
|
|
/**
|
|
|
|
|
*
|
|
|
|
|
*/
|
2016-01-06 16:39:13 -06:00
|
|
|
class ConferenceConnector {
|
2017-10-12 18:02:29 -05:00
|
|
|
/**
|
|
|
|
|
*
|
|
|
|
|
*/
|
2022-12-13 08:26:22 -06:00
|
|
|
constructor(resolve, reject, conference) {
|
|
|
|
|
this._conference = conference;
|
2016-01-06 16:39:13 -06:00
|
|
|
this._resolve = resolve;
|
|
|
|
|
this._reject = reject;
|
|
|
|
|
this.reconnectTimeout = null;
|
2017-10-10 18:31:40 -05:00
|
|
|
room.on(JitsiConferenceEvents.CONFERENCE_JOINED,
|
2016-01-06 16:39:13 -06:00
|
|
|
this._handleConferenceJoined.bind(this));
|
2017-10-10 18:31:40 -05:00
|
|
|
room.on(JitsiConferenceEvents.CONFERENCE_FAILED,
|
2016-01-06 16:39:13 -06:00
|
|
|
this._onConferenceFailed.bind(this));
|
|
|
|
|
}
|
2017-10-12 18:02:29 -05:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
*
|
|
|
|
|
*/
|
2016-10-03 11:12:04 -05:00
|
|
|
_handleConferenceFailed(err) {
|
2016-01-06 16:39:13 -06:00
|
|
|
this._unsubscribe();
|
|
|
|
|
this._reject(err);
|
|
|
|
|
}
|
2017-10-12 18:02:29 -05:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
*
|
|
|
|
|
*/
|
2016-01-25 16:39:05 -06:00
|
|
|
_onConferenceFailed(err, ...params) {
|
2017-01-31 14:58:48 -06:00
|
|
|
APP.store.dispatch(conferenceFailed(room, err, ...params));
|
2016-11-11 09:00:54 -06:00
|
|
|
logger.error('CONFERENCE FAILED:', err, ...params);
|
2016-01-06 16:39:13 -06:00
|
|
|
|
2017-06-05 13:19:25 -05:00
|
|
|
switch (err) {
|
2016-01-06 16:39:13 -06:00
|
|
|
|
2017-10-10 18:31:40 -05:00
|
|
|
case JitsiConferenceErrors.RESERVATION_ERROR: {
|
2017-10-12 18:02:29 -05:00
|
|
|
const [ code, msg ] = params;
|
|
|
|
|
|
2023-07-15 17:33:26 -05:00
|
|
|
APP.store.dispatch(showErrorNotification({
|
|
|
|
|
descriptionArguments: {
|
|
|
|
|
code,
|
|
|
|
|
msg
|
|
|
|
|
},
|
|
|
|
|
descriptionKey: 'dialog.reservationErrorMsg',
|
|
|
|
|
titleKey: 'dialog.reservationError'
|
2025-02-07 14:36:46 -06:00
|
|
|
}));
|
2022-12-13 08:26:22 -06:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
2017-10-10 18:31:40 -05:00
|
|
|
case JitsiConferenceErrors.GRACEFUL_SHUTDOWN:
|
2023-07-15 17:33:26 -05:00
|
|
|
APP.store.dispatch(showErrorNotification({
|
|
|
|
|
descriptionKey: 'dialog.gracefulShutdown',
|
|
|
|
|
titleKey: 'dialog.serviceUnavailable'
|
2025-02-07 14:36:46 -06:00
|
|
|
}));
|
2016-01-25 16:39:05 -06:00
|
|
|
break;
|
|
|
|
|
|
2017-10-02 18:08:07 -05:00
|
|
|
// FIXME FOCUS_DISCONNECTED is a confusing event name.
|
|
|
|
|
// What really happens there is that the library is not ready yet,
|
|
|
|
|
// because Jicofo is not available, but it is going to give it another
|
|
|
|
|
// try.
|
2017-10-10 18:31:40 -05:00
|
|
|
case JitsiConferenceErrors.FOCUS_DISCONNECTED: {
|
2017-10-12 18:02:29 -05:00
|
|
|
const [ focus, retrySec ] = params;
|
|
|
|
|
|
2021-11-24 13:05:27 +02:00
|
|
|
APP.store.dispatch(showNotification({
|
|
|
|
|
descriptionKey: focus,
|
|
|
|
|
titleKey: retrySec
|
|
|
|
|
}, NOTIFICATION_TIMEOUT_TYPE.SHORT));
|
2016-01-25 16:39:05 -06:00
|
|
|
break;
|
2017-10-02 18:08:07 -05:00
|
|
|
}
|
2016-01-25 16:39:05 -06:00
|
|
|
|
2017-10-10 18:31:40 -05:00
|
|
|
case JitsiConferenceErrors.FOCUS_LEFT:
|
2020-05-07 06:59:37 -05:00
|
|
|
case JitsiConferenceErrors.ICE_FAILED:
|
2017-10-10 18:31:40 -05:00
|
|
|
case JitsiConferenceErrors.VIDEOBRIDGE_NOT_AVAILABLE:
|
2019-06-17 11:35:47 +01:00
|
|
|
case JitsiConferenceErrors.OFFER_ANSWER_FAILED:
|
2018-08-01 15:37:15 -05:00
|
|
|
APP.store.dispatch(conferenceWillLeave(room));
|
|
|
|
|
|
2016-10-06 13:30:00 -05:00
|
|
|
// FIXME the conference should be stopped by the library and not by
|
|
|
|
|
// the app. Both the errors above are unrecoverable from the library
|
|
|
|
|
// perspective.
|
2023-07-15 17:33:26 -05:00
|
|
|
room.leave(CONFERENCE_LEAVE_REASONS.UNRECOVERABLE_ERROR).then(() => APP.connection.disconnect());
|
2016-02-05 17:04:48 +02:00
|
|
|
break;
|
|
|
|
|
|
2017-10-10 18:31:40 -05:00
|
|
|
case JitsiConferenceErrors.INCOMPATIBLE_SERVER_VERSIONS:
|
2018-02-26 16:50:27 -06:00
|
|
|
APP.store.dispatch(reloadWithStoredParams());
|
2016-07-07 20:44:04 -05:00
|
|
|
break;
|
2017-10-02 18:08:07 -05:00
|
|
|
|
2016-01-06 16:39:13 -06:00
|
|
|
default:
|
2016-01-25 16:39:05 -06:00
|
|
|
this._handleConferenceFailed(err, ...params);
|
|
|
|
|
}
|
|
|
|
|
}
|
2017-10-12 18:02:29 -05:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
*
|
|
|
|
|
*/
|
2016-01-06 16:39:13 -06:00
|
|
|
_unsubscribe() {
|
|
|
|
|
room.off(
|
2017-10-12 18:02:29 -05:00
|
|
|
JitsiConferenceEvents.CONFERENCE_JOINED,
|
|
|
|
|
this._handleConferenceJoined);
|
2016-01-06 16:39:13 -06:00
|
|
|
room.off(
|
2017-10-12 18:02:29 -05:00
|
|
|
JitsiConferenceEvents.CONFERENCE_FAILED,
|
|
|
|
|
this._onConferenceFailed);
|
2016-01-06 16:39:13 -06:00
|
|
|
if (this.reconnectTimeout !== null) {
|
|
|
|
|
clearTimeout(this.reconnectTimeout);
|
|
|
|
|
}
|
|
|
|
|
}
|
2017-10-12 18:02:29 -05:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
*
|
|
|
|
|
*/
|
2016-01-06 16:39:13 -06:00
|
|
|
_handleConferenceJoined() {
|
|
|
|
|
this._unsubscribe();
|
|
|
|
|
this._resolve();
|
|
|
|
|
}
|
2017-10-12 18:02:29 -05:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
*
|
|
|
|
|
*/
|
2016-01-06 16:39:13 -06:00
|
|
|
connect() {
|
2021-06-16 14:08:18 +03:00
|
|
|
const replaceParticipant = getReplaceParticipant(APP.store.getState());
|
2021-06-11 11:58:45 +03:00
|
|
|
|
2021-04-07 10:03:20 -05:00
|
|
|
// the local storage overrides here and in connection.js can be used by jibri
|
2021-06-11 11:58:45 +03:00
|
|
|
room.join(jitsiLocalStorage.getItem('xmpp_conference_password_override'), replaceParticipant);
|
2016-01-06 16:39:13 -06:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-10-12 16:30:44 -05:00
|
|
|
/**
|
|
|
|
|
* Disconnects the connection.
|
|
|
|
|
* @returns resolved Promise. We need this in order to make the Promise.all
|
|
|
|
|
* call in hangup() to resolve when all operations are finished.
|
|
|
|
|
*/
|
|
|
|
|
function disconnect() {
|
2019-05-30 14:24:58 +01:00
|
|
|
const onDisconnected = () => {
|
|
|
|
|
APP.API.notifyConferenceLeft(APP.conference.roomName);
|
2017-10-12 18:02:29 -05:00
|
|
|
|
2019-05-30 14:24:58 +01:00
|
|
|
return Promise.resolve();
|
|
|
|
|
};
|
|
|
|
|
|
2023-07-15 17:33:26 -05:00
|
|
|
if (!APP.connection) {
|
2020-07-15 15:18:03 +03:00
|
|
|
return onDisconnected();
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-15 17:33:26 -05:00
|
|
|
return APP.connection.disconnect().then(onDisconnected, onDisconnected);
|
2016-10-12 16:30:44 -05:00
|
|
|
}
|
|
|
|
|
|
2016-01-06 16:39:13 -06:00
|
|
|
export default {
|
2017-07-21 11:12:33 +02:00
|
|
|
/**
|
|
|
|
|
* Flag used to delay modification of the muted status of local media tracks
|
|
|
|
|
* until those are created (or not, but at that point it's certain that
|
|
|
|
|
* the tracks won't exist).
|
|
|
|
|
*/
|
|
|
|
|
_localTracksInitialized: false,
|
2017-10-12 18:02:29 -05:00
|
|
|
|
2022-09-29 11:26:07 -04:00
|
|
|
/**
|
|
|
|
|
* Flag used to prevent the creation of another local video track in this.muteVideo if one is already in progress.
|
|
|
|
|
*/
|
|
|
|
|
isCreatingLocalTrack: false,
|
|
|
|
|
|
2020-11-03 10:44:41 +01:00
|
|
|
isSharingScreen: false,
|
2017-10-12 18:02:29 -05:00
|
|
|
|
2017-07-17 13:38:46 +02:00
|
|
|
/**
|
2020-04-16 13:47:10 +03:00
|
|
|
* Returns an object containing a promise which resolves with the created tracks &
|
|
|
|
|
* the errors resulting from that process.
|
2023-07-15 17:33:26 -05:00
|
|
|
* @param {object} options
|
|
|
|
|
* @param {boolean} options.startAudioOnly=false - if <tt>true</tt> then
|
|
|
|
|
* only audio track will be created and the audio only mode will be turned
|
|
|
|
|
* on.
|
|
|
|
|
* @param {boolean} options.startScreenSharing=false - if <tt>true</tt>
|
|
|
|
|
* should start with screensharing instead of camera video.
|
|
|
|
|
* @param {boolean} options.startWithAudioMuted - will start the conference
|
|
|
|
|
* without any audio tracks.
|
|
|
|
|
* @param {boolean} options.startWithVideoMuted - will start the conference
|
|
|
|
|
* without any video tracks.
|
2024-10-31 12:49:57 -05:00
|
|
|
* @param {boolean} recordTimeMetrics - If true time metrics will be recorded.
|
2020-04-16 13:47:10 +03:00
|
|
|
* @returns {Promise<JitsiLocalTrack[]>, Object}
|
2017-07-17 13:38:46 +02:00
|
|
|
*/
|
2024-10-31 12:49:57 -05:00
|
|
|
createInitialLocalTracks(options = {}, recordTimeMetrics = false) {
|
2020-04-16 13:47:10 +03:00
|
|
|
const errors = {};
|
2021-03-05 10:18:34 -05:00
|
|
|
|
|
|
|
|
// Always get a handle on the audio input device so that we have statistics (such as "No audio input" or
|
|
|
|
|
// "Are you trying to speak?" ) even if the user joins the conference muted.
|
2023-08-17 09:47:48 +03:00
|
|
|
const initialDevices = config.startSilent || config.disableInitialGUM ? [] : [ MEDIA_TYPE.AUDIO ];
|
2021-01-11 13:16:49 +02:00
|
|
|
const requestedAudio = !config.disableInitialGUM;
|
2017-07-24 18:10:31 +02:00
|
|
|
let requestedVideo = false;
|
|
|
|
|
|
2021-01-11 13:16:49 +02:00
|
|
|
if (!config.disableInitialGUM
|
|
|
|
|
&& !options.startWithVideoMuted
|
2017-07-24 18:10:31 +02:00
|
|
|
&& !options.startAudioOnly
|
|
|
|
|
&& !options.startScreenSharing) {
|
2022-01-25 14:41:53 -05:00
|
|
|
initialDevices.push(MEDIA_TYPE.VIDEO);
|
2017-07-24 18:10:31 +02:00
|
|
|
requestedVideo = true;
|
|
|
|
|
}
|
2017-07-17 13:38:46 +02:00
|
|
|
|
2023-05-03 18:16:48 -04:00
|
|
|
let tryCreateLocalTracks = Promise.resolve([]);
|
2017-06-29 19:43:35 +02:00
|
|
|
|
2021-02-08 15:10:24 -06:00
|
|
|
// On Electron there is no permission prompt for granting permissions. That's why we don't need to
|
2021-03-16 11:59:33 -04:00
|
|
|
// spend much time displaying the overlay screen. If GUM is not resolved within 15 seconds it will
|
2021-02-08 15:10:24 -06:00
|
|
|
// probably never resolve.
|
2021-02-01 18:20:39 -06:00
|
|
|
const timeout = browser.isElectron() ? 15000 : 60000;
|
2022-01-25 14:41:53 -05:00
|
|
|
const audioOptions = {
|
|
|
|
|
devices: [ MEDIA_TYPE.AUDIO ],
|
2024-12-16 16:12:56 -05:00
|
|
|
timeout
|
2022-01-25 14:41:53 -05:00
|
|
|
};
|
2021-02-01 18:20:39 -06:00
|
|
|
|
2022-11-08 14:15:49 -05:00
|
|
|
// Spot uses the _desktopSharingSourceDevice config option to use an external video input device label as
|
|
|
|
|
// screenshare and calls getUserMedia instead of getDisplayMedia for capturing the media.
|
|
|
|
|
if (options.startScreenSharing && config._desktopSharingSourceDevice) {
|
|
|
|
|
tryCreateLocalTracks = this._createDesktopTrack()
|
2020-04-16 13:47:10 +03:00
|
|
|
.then(([ desktopStream ]) => {
|
2017-07-24 18:10:31 +02:00
|
|
|
if (!requestedAudio) {
|
2017-10-12 18:02:29 -05:00
|
|
|
return [ desktopStream ];
|
2017-07-24 18:10:31 +02:00
|
|
|
}
|
|
|
|
|
|
2022-01-25 14:41:53 -05:00
|
|
|
return createLocalTracksF(audioOptions)
|
2017-10-12 18:02:29 -05:00
|
|
|
.then(([ audioStream ]) =>
|
|
|
|
|
[ desktopStream, audioStream ])
|
2017-06-29 19:43:35 +02:00
|
|
|
.catch(error => {
|
2020-04-16 13:47:10 +03:00
|
|
|
errors.audioOnlyError = error;
|
2017-10-12 18:02:29 -05:00
|
|
|
|
|
|
|
|
return [ desktopStream ];
|
2017-06-29 19:43:35 +02:00
|
|
|
});
|
2017-10-12 18:02:29 -05:00
|
|
|
})
|
|
|
|
|
.catch(error => {
|
2017-06-29 19:43:35 +02:00
|
|
|
logger.error('Failed to obtain desktop stream', error);
|
2020-04-16 13:47:10 +03:00
|
|
|
errors.screenSharingError = error;
|
2017-10-12 18:02:29 -05:00
|
|
|
|
2022-01-25 14:41:53 -05:00
|
|
|
return requestedAudio ? createLocalTracksF(audioOptions) : [];
|
2017-10-12 18:02:29 -05:00
|
|
|
})
|
|
|
|
|
.catch(error => {
|
2020-04-16 13:47:10 +03:00
|
|
|
errors.audioOnlyError = error;
|
2017-10-12 18:02:29 -05:00
|
|
|
|
2017-06-29 19:43:35 +02:00
|
|
|
return [];
|
|
|
|
|
});
|
2023-05-03 18:16:48 -04:00
|
|
|
} else if (requestedAudio || requestedVideo) {
|
2024-05-07 15:32:45 -05:00
|
|
|
tryCreateLocalTracks = APP.store.dispatch(createInitialAVTracks({
|
2021-02-01 18:20:39 -06:00
|
|
|
devices: initialDevices,
|
2024-12-16 16:12:56 -05:00
|
|
|
timeout
|
2024-10-31 12:49:57 -05:00
|
|
|
}, recordTimeMetrics)).then(({ tracks, errors: pErrors }) => {
|
2024-05-07 15:32:45 -05:00
|
|
|
Object.assign(errors, pErrors);
|
2023-05-03 18:16:48 -04:00
|
|
|
|
|
|
|
|
return tracks;
|
|
|
|
|
});
|
2017-06-29 19:43:35 +02:00
|
|
|
}
|
2017-07-17 13:38:46 +02:00
|
|
|
|
2020-04-16 13:47:10 +03:00
|
|
|
return {
|
|
|
|
|
tryCreateLocalTracks,
|
|
|
|
|
errors
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
|
2023-07-15 17:33:26 -05:00
|
|
|
startConference(tracks) {
|
2020-04-16 13:47:10 +03:00
|
|
|
tracks.forEach(track => {
|
|
|
|
|
if ((track.isAudioTrack() && this.isLocalAudioMuted())
|
|
|
|
|
|| (track.isVideoTrack() && this.isLocalVideoMuted())) {
|
|
|
|
|
const mediaType = track.getType();
|
|
|
|
|
|
|
|
|
|
sendAnalytics(
|
|
|
|
|
createTrackMutedEvent(mediaType, 'initial mute'));
|
|
|
|
|
logger.log(`${mediaType} mute: initially muted.`);
|
|
|
|
|
track.mute();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this._createRoom(tracks);
|
|
|
|
|
|
|
|
|
|
// if user didn't give access to mic or camera or doesn't have
|
|
|
|
|
// them at all, we mark corresponding toolbar buttons as muted,
|
|
|
|
|
// so that the user can try unmute later on and add audio/video
|
|
|
|
|
// to the conference
|
|
|
|
|
if (!tracks.find(t => t.isAudioTrack())) {
|
2023-10-09 15:37:50 -05:00
|
|
|
this.updateAudioIconEnabled();
|
2020-04-16 13:47:10 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!tracks.find(t => t.isVideoTrack())) {
|
2021-05-04 15:57:34 +03:00
|
|
|
this.setVideoMuteStatus();
|
2020-04-16 13:47:10 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (config.iAmRecorder) {
|
|
|
|
|
this.recorder = new Recorder();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (config.startSilent) {
|
|
|
|
|
sendAnalytics(createStartSilentEvent());
|
|
|
|
|
APP.store.dispatch(showNotification({
|
|
|
|
|
descriptionKey: 'notify.startSilentDescription',
|
|
|
|
|
titleKey: 'notify.startSilentTitle'
|
2021-11-24 13:05:27 +02:00
|
|
|
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
|
2020-04-16 13:47:10 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// XXX The API will take care of disconnecting from the XMPP
|
|
|
|
|
// server (and, thus, leaving the room) on unload.
|
|
|
|
|
return new Promise((resolve, reject) => {
|
2022-12-13 08:26:22 -06:00
|
|
|
new ConferenceConnector(resolve, reject, this).connect();
|
2020-04-16 13:47:10 +03:00
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
|
2016-01-15 16:59:35 +02:00
|
|
|
/**
|
2020-04-16 13:47:10 +03:00
|
|
|
* Open new connection and join the conference when prejoin page is not enabled.
|
|
|
|
|
* If prejoin page is enabled open an new connection in the background
|
|
|
|
|
* and create local tracks.
|
|
|
|
|
*
|
2024-10-25 10:28:31 -05:00
|
|
|
* @param {{ roomName: string, shouldDispatchConnect }} options
|
2016-01-15 16:59:35 +02:00
|
|
|
* @returns {Promise}
|
|
|
|
|
*/
|
2024-10-25 10:28:31 -05:00
|
|
|
async init({ roomName, shouldDispatchConnect }) {
|
2023-04-21 14:39:37 -04:00
|
|
|
const state = APP.store.getState();
|
2020-04-16 13:47:10 +03:00
|
|
|
const initialOptions = {
|
|
|
|
|
startAudioOnly: config.startAudioOnly,
|
|
|
|
|
startScreenSharing: config.startScreenSharing,
|
2023-05-03 18:16:48 -04:00
|
|
|
startWithAudioMuted: getStartWithAudioMuted(state) || isUserInteractionRequiredForUnmute(state),
|
|
|
|
|
startWithVideoMuted: getStartWithVideoMuted(state) || isUserInteractionRequiredForUnmute(state)
|
2020-04-16 13:47:10 +03:00
|
|
|
};
|
2024-10-31 12:49:57 -05:00
|
|
|
const connectionTimes = getJitsiMeetGlobalNSConnectionTimes();
|
|
|
|
|
const startTime = window.performance.now();
|
2017-10-12 18:02:29 -05:00
|
|
|
|
2024-10-31 12:49:57 -05:00
|
|
|
connectionTimes['conference.init.start'] = startTime;
|
|
|
|
|
|
|
|
|
|
logger.debug(`Executed conference.init with roomName: ${roomName} (performance.now=${startTime})`);
|
2024-02-19 19:56:15 -06:00
|
|
|
|
2020-04-16 13:47:10 +03:00
|
|
|
this.roomName = roomName;
|
2020-04-07 14:14:47 +02:00
|
|
|
|
2020-04-16 13:47:10 +03:00
|
|
|
try {
|
2023-05-03 18:16:48 -04:00
|
|
|
// Initialize the device list first. This way, when creating tracks based on preferred devices, loose label
|
|
|
|
|
// matching can be done in cases where the exact ID match is no longer available, such as -
|
|
|
|
|
// 1. When the camera device has switched USB ports.
|
|
|
|
|
// 2. When in startSilent mode we want to start with audio muted
|
2020-04-16 13:47:10 +03:00
|
|
|
await this._initDeviceList();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.warn('initial device list initialization failed', error);
|
|
|
|
|
}
|
2017-02-16 17:02:40 -06:00
|
|
|
|
2023-05-03 18:16:48 -04:00
|
|
|
// Filter out the local tracks based on various config options, i.e., when user joins muted or is muted by
|
|
|
|
|
// focus. However, audio track will always be created even though it is not added to the conference since we
|
|
|
|
|
// want audio related features (noisy mic, talk while muted, etc.) to work even if the mic is muted.
|
|
|
|
|
const handleInitialTracks = (options, tracks) => {
|
|
|
|
|
let localTracks = tracks;
|
|
|
|
|
|
2024-04-29 18:39:09 -05:00
|
|
|
if (options.startWithAudioMuted) {
|
2022-01-07 10:25:01 -06:00
|
|
|
// Always add the track on Safari because of a known issue where audio playout doesn't happen
|
|
|
|
|
// if the user joins audio and video muted, i.e., if there is no local media capture.
|
|
|
|
|
if (browser.isWebKitBased()) {
|
|
|
|
|
this.muteAudio(true, true);
|
|
|
|
|
} else {
|
2025-09-05 22:52:35 +02:00
|
|
|
localTracks = localTracks.filter(track => {
|
|
|
|
|
if (track.getType() === MEDIA_TYPE.AUDIO) {
|
|
|
|
|
track.stopStream();
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
});
|
2022-01-07 10:25:01 -06:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-03 18:16:48 -04:00
|
|
|
return localTracks;
|
2022-01-07 10:25:01 -06:00
|
|
|
};
|
2024-04-29 18:39:09 -05:00
|
|
|
const { dispatch, getState } = APP.store;
|
2024-10-31 12:49:57 -05:00
|
|
|
const createLocalTracksStart = window.performance.now();
|
|
|
|
|
|
|
|
|
|
connectionTimes['conference.init.createLocalTracks.start'] = createLocalTracksStart;
|
|
|
|
|
|
|
|
|
|
logger.debug(`(TIME) createInitialLocalTracks: ${createLocalTracksStart} `);
|
|
|
|
|
|
|
|
|
|
const { tryCreateLocalTracks, errors } = this.createInitialLocalTracks(initialOptions, true);
|
2022-01-07 10:25:01 -06:00
|
|
|
|
2025-03-17 14:08:55 +01:00
|
|
|
tryCreateLocalTracks.then(tr => {
|
2024-10-31 12:49:57 -05:00
|
|
|
const createLocalTracksEnd = window.performance.now();
|
|
|
|
|
|
|
|
|
|
connectionTimes['conference.init.createLocalTracks.end'] = createLocalTracksEnd;
|
|
|
|
|
logger.debug(`(TIME) createInitialLocalTracks finished: ${createLocalTracksEnd} `);
|
2024-04-29 18:39:09 -05:00
|
|
|
const tracks = handleInitialTracks(initialOptions, tr);
|
2016-06-23 11:03:26 +03:00
|
|
|
|
2020-04-16 13:47:10 +03:00
|
|
|
this._initDeviceList(true);
|
2016-06-21 12:08:32 +03:00
|
|
|
|
2024-10-25 10:28:31 -05:00
|
|
|
const { initialGUMPromise } = getState()['features/base/media'];
|
|
|
|
|
|
2024-04-29 18:39:09 -05:00
|
|
|
if (isPrejoinPageVisible(getState())) {
|
2024-02-12 18:35:51 -06:00
|
|
|
dispatch(gumPending([ MEDIA_TYPE.AUDIO, MEDIA_TYPE.VIDEO ], IGUMPendingState.NONE));
|
2024-10-25 10:28:31 -05:00
|
|
|
|
|
|
|
|
// 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.
|
2024-06-25 12:27:02 +03:00
|
|
|
initPrejoin(tracks, errors, dispatch);
|
2024-10-25 10:28:31 -05:00
|
|
|
|
2024-10-31 12:49:57 -05:00
|
|
|
connectionTimes['conference.init.end'] = window.performance.now();
|
|
|
|
|
|
2024-10-25 10:28:31 -05:00
|
|
|
// 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());
|
2024-04-29 18:39:09 -05:00
|
|
|
} else {
|
|
|
|
|
APP.store.dispatch(displayErrorsForCreateInitialLocalTracks(errors));
|
|
|
|
|
setGUMPendingStateOnFailedTracks(tracks, APP.store.dispatch);
|
2020-12-09 11:40:23 -06:00
|
|
|
|
2024-10-31 12:49:57 -05:00
|
|
|
connectionTimes['conference.init.end'] = window.performance.now();
|
2024-10-25 10:28:31 -05:00
|
|
|
if (initialGUMPromise) {
|
|
|
|
|
logger.debug('Resolving the initialGUM promise!');
|
|
|
|
|
initialGUMPromise.resolve({
|
|
|
|
|
tracks,
|
|
|
|
|
errors
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
2020-12-09 11:40:23 -06:00
|
|
|
|
2024-10-25 10:28:31 -05:00
|
|
|
if (shouldDispatchConnect) {
|
2024-10-15 17:56:34 -05:00
|
|
|
logger.info('Dispatching connect from init since prejoin is not visible.');
|
2024-04-29 18:39:09 -05:00
|
|
|
dispatch(connect());
|
2020-04-16 13:47:10 +03:00
|
|
|
}
|
2016-01-06 16:39:13 -06:00
|
|
|
},
|
2017-10-12 18:02:29 -05:00
|
|
|
|
2016-01-15 16:59:35 +02:00
|
|
|
/**
|
|
|
|
|
* Check if id is id of the local user.
|
|
|
|
|
* @param {string} id id to check
|
|
|
|
|
* @returns {boolean}
|
|
|
|
|
*/
|
2017-04-11 14:40:03 -05:00
|
|
|
isLocalId(id) {
|
2016-07-07 20:44:04 -05:00
|
|
|
return this.getMyUserId() === id;
|
2016-01-06 16:39:13 -06:00
|
|
|
},
|
2017-08-18 12:30:30 +01:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Tells whether the local video is muted or not.
|
|
|
|
|
* @return {boolean}
|
|
|
|
|
*/
|
|
|
|
|
isLocalVideoMuted() {
|
|
|
|
|
// If the tracks are not ready, read from base/media state
|
|
|
|
|
return this._localTracksInitialized
|
2022-11-08 14:15:49 -05:00
|
|
|
? isLocalTrackMuted(APP.store.getState()['features/base/tracks'], MEDIA_TYPE.VIDEO)
|
2017-08-18 12:30:30 +01:00
|
|
|
: isVideoMutedByUser(APP.store);
|
|
|
|
|
},
|
|
|
|
|
|
2022-05-09 12:37:50 +03:00
|
|
|
/**
|
|
|
|
|
* Verify if there is an ongoing system audio sharing session and apply to the provided track
|
|
|
|
|
* as a AudioMixer effect.
|
|
|
|
|
*
|
|
|
|
|
* @param {*} localAudioTrack - track to which system audio track will be applied as an effect, most likely
|
|
|
|
|
* microphone local audio track.
|
|
|
|
|
*/
|
|
|
|
|
async _maybeApplyAudioMixerEffect(localAudioTrack) {
|
|
|
|
|
|
|
|
|
|
// At the time of writing this comment there were two separate flows for toggling screen-sharing
|
|
|
|
|
// and system audio sharing, the first is the legacy method using the functionality from conference.js
|
|
|
|
|
// the second is used when both sendMultipleVideoStreams and sourceNameSignaling flags are set to true.
|
|
|
|
|
// The second flow uses functionality from base/conference/middleware.web.js.
|
|
|
|
|
// We check if system audio sharing was done using the first flow by verifying this._desktopAudioStream and
|
|
|
|
|
// for the second by checking 'features/screen-share' state.
|
|
|
|
|
const { desktopAudioTrack } = APP.store.getState()['features/screen-share'];
|
|
|
|
|
const currentDesktopAudioTrack = this._desktopAudioStream || desktopAudioTrack;
|
|
|
|
|
|
|
|
|
|
// If system audio is already being sent, mix it with the provided audio track.
|
|
|
|
|
if (currentDesktopAudioTrack) {
|
|
|
|
|
// In case system audio sharing was done in the absence of an initial mic audio track, there is no
|
|
|
|
|
// AudioMixerEffect so we have to remove system audio track from the room before setting it as an effect.
|
|
|
|
|
await room.replaceTrack(currentDesktopAudioTrack, null);
|
|
|
|
|
this._mixerEffect = new AudioMixerEffect(currentDesktopAudioTrack);
|
|
|
|
|
logger.debug('Mixing new audio track with existing screen audio track.');
|
|
|
|
|
await localAudioTrack.setEffect(this._mixerEffect);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2016-01-06 16:39:13 -06:00
|
|
|
/**
|
|
|
|
|
* Simulates toolbar button click for audio mute. Used by shortcuts and API.
|
2024-07-23 16:57:44 -05:00
|
|
|
*
|
2017-07-25 12:05:08 +02:00
|
|
|
* @param {boolean} mute true for mute and false for unmute.
|
|
|
|
|
* dialogs in case of media permissions error.
|
2024-07-23 16:57:44 -05:00
|
|
|
* @returns {Promise}
|
2016-01-06 16:39:13 -06:00
|
|
|
*/
|
2025-06-09 23:44:24 +03:00
|
|
|
async muteAudio(mute) {
|
2021-09-15 11:28:44 +03:00
|
|
|
const state = APP.store.getState();
|
|
|
|
|
|
2019-07-10 04:02:27 -07:00
|
|
|
if (!mute
|
2021-09-15 11:28:44 +03:00
|
|
|
&& isUserInteractionRequiredForUnmute(state)) {
|
2019-07-10 04:02:27 -07:00
|
|
|
logger.error('Unmuting audio requires user interaction');
|
|
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-09 23:44:24 +03:00
|
|
|
await APP.store.dispatch(setAudioMuted(mute, true));
|
2016-01-06 16:39:13 -06:00
|
|
|
},
|
2017-10-12 18:02:29 -05:00
|
|
|
|
2016-02-09 10:29:50 -06:00
|
|
|
/**
|
|
|
|
|
* Returns whether local audio is muted or not.
|
|
|
|
|
* @returns {boolean}
|
|
|
|
|
*/
|
|
|
|
|
isLocalAudioMuted() {
|
2017-08-18 12:30:30 +01:00
|
|
|
// If the tracks are not ready, read from base/media state
|
|
|
|
|
return this._localTracksInitialized
|
|
|
|
|
? isLocalTrackMuted(
|
|
|
|
|
APP.store.getState()['features/base/tracks'],
|
|
|
|
|
MEDIA_TYPE.AUDIO)
|
|
|
|
|
: Boolean(
|
|
|
|
|
APP.store.getState()['features/base/media'].audio.muted);
|
2016-02-09 10:29:50 -06:00
|
|
|
},
|
2017-10-12 18:02:29 -05:00
|
|
|
|
2016-01-06 16:39:13 -06:00
|
|
|
/**
|
2017-04-04 15:55:03 -05:00
|
|
|
* Simulates toolbar button click for audio mute. Used by shortcuts
|
|
|
|
|
* and API.
|
2017-07-25 12:05:08 +02:00
|
|
|
* @param {boolean} [showUI] when set to false will not display any error
|
|
|
|
|
* dialogs in case of media permissions error.
|
2016-01-06 16:39:13 -06:00
|
|
|
*/
|
2017-07-25 12:05:08 +02:00
|
|
|
toggleAudioMuted(showUI = true) {
|
2017-08-18 12:30:30 +01:00
|
|
|
this.muteAudio(!this.isLocalAudioMuted(), showUI);
|
2016-01-06 16:39:13 -06:00
|
|
|
},
|
2017-10-12 18:02:29 -05:00
|
|
|
|
2016-01-06 16:39:13 -06:00
|
|
|
/**
|
|
|
|
|
* Simulates toolbar button click for video mute. Used by shortcuts and API.
|
|
|
|
|
* @param mute true for mute and false for unmute.
|
2017-07-20 14:29:15 +02:00
|
|
|
* dialogs in case of media permissions error.
|
2016-01-06 16:39:13 -06:00
|
|
|
*/
|
2025-06-09 23:44:24 +03:00
|
|
|
muteVideo(mute) {
|
2023-03-09 16:31:12 -06:00
|
|
|
const state = APP.store.getState();
|
|
|
|
|
|
2019-07-10 04:02:27 -07:00
|
|
|
if (!mute
|
2023-03-09 16:31:12 -06:00
|
|
|
&& isUserInteractionRequiredForUnmute(state)) {
|
2019-07-10 04:02:27 -07:00
|
|
|
logger.error('Unmuting video requires user interaction');
|
|
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-09 23:44:24 +03:00
|
|
|
APP.store.dispatch(setVideoMuted(mute, VIDEO_MUTISM_AUTHORITY.USER, true));
|
2016-01-06 16:39:13 -06:00
|
|
|
},
|
2017-10-12 18:02:29 -05:00
|
|
|
|
2016-01-06 16:39:13 -06:00
|
|
|
/**
|
|
|
|
|
* Simulates toolbar button click for video mute. Used by shortcuts and API.
|
2017-07-20 14:29:15 +02:00
|
|
|
* @param {boolean} [showUI] when set to false will not display any error
|
|
|
|
|
* dialogs in case of media permissions error.
|
2022-08-30 11:42:29 +03:00
|
|
|
* @param {boolean} ensureTrack - True if we want to ensure that a new track is
|
|
|
|
|
* created if missing.
|
2016-01-06 16:39:13 -06:00
|
|
|
*/
|
2022-08-30 11:42:29 +03:00
|
|
|
toggleVideoMuted(showUI = true, ensureTrack = false) {
|
|
|
|
|
const mute = !this.isLocalVideoMuted();
|
|
|
|
|
|
|
|
|
|
APP.store.dispatch(handleToggleVideoMuted(mute, showUI, ensureTrack));
|
2016-01-06 16:39:13 -06:00
|
|
|
},
|
2017-10-12 18:02:29 -05:00
|
|
|
|
2016-01-15 16:59:35 +02:00
|
|
|
/**
|
|
|
|
|
* Retrieve list of ids of conference participants (without local user).
|
|
|
|
|
* @returns {string[]}
|
|
|
|
|
*/
|
2017-04-11 14:40:03 -05:00
|
|
|
listMembersIds() {
|
2016-01-06 16:39:13 -06:00
|
|
|
return room.getParticipants().map(p => p.getId());
|
|
|
|
|
},
|
2017-10-12 18:02:29 -05:00
|
|
|
|
2016-03-24 15:25:26 -05:00
|
|
|
/**
|
|
|
|
|
* Checks whether the participant identified by id is a moderator.
|
|
|
|
|
* @id id to search for participant
|
|
|
|
|
* @return {boolean} whether the participant is moderator
|
|
|
|
|
*/
|
2017-04-11 14:40:03 -05:00
|
|
|
isParticipantModerator(id) {
|
2017-10-12 18:02:29 -05:00
|
|
|
const user = room.getParticipantById(id);
|
|
|
|
|
|
2016-03-24 15:25:26 -05:00
|
|
|
return user && user.isModerator();
|
|
|
|
|
},
|
2017-10-16 15:37:13 -05:00
|
|
|
|
2020-02-21 09:17:11 +00:00
|
|
|
/**
|
|
|
|
|
* Retrieve list of conference participants (without local user).
|
|
|
|
|
* @returns {JitsiParticipant[]}
|
|
|
|
|
*
|
|
|
|
|
* NOTE: Used by jitsi-meet-torture!
|
|
|
|
|
*/
|
|
|
|
|
listMembers() {
|
|
|
|
|
return room.getParticipants();
|
|
|
|
|
},
|
|
|
|
|
|
2022-02-01 14:44:20 +01:00
|
|
|
/**
|
|
|
|
|
* Used by Jibri to detect when it's alone and the meeting should be terminated.
|
|
|
|
|
*/
|
2017-04-11 14:40:03 -05:00
|
|
|
get membersCount() {
|
2022-02-17 16:25:31 -06:00
|
|
|
return room.getParticipants()
|
|
|
|
|
.filter(p => !p.isHidden() || !(config.iAmRecorder && p.isHiddenFromRecorder())).length + 1;
|
2015-12-31 17:23:23 +02:00
|
|
|
},
|
2017-10-12 18:02:29 -05:00
|
|
|
|
2017-02-16 16:59:30 -08:00
|
|
|
/**
|
|
|
|
|
* Get speaker stats that track total dominant speaker time.
|
|
|
|
|
*
|
|
|
|
|
* @returns {object} A hash with keys being user ids and values being the
|
|
|
|
|
* library's SpeakerStats model used for calculating time as dominant
|
|
|
|
|
* speaker.
|
|
|
|
|
*/
|
|
|
|
|
getSpeakerStats() {
|
|
|
|
|
return room.getSpeakerStats();
|
|
|
|
|
},
|
|
|
|
|
|
2016-01-06 16:39:13 -06:00
|
|
|
// used by torture currently
|
2017-04-11 14:40:03 -05:00
|
|
|
isJoined() {
|
2019-07-02 14:11:56 +01:00
|
|
|
return room && room.isJoined();
|
2016-01-06 16:39:13 -06:00
|
|
|
},
|
2017-04-11 14:40:03 -05:00
|
|
|
getConnectionState() {
|
2019-07-02 14:11:56 +01:00
|
|
|
return room && room.getConnectionState();
|
2016-01-06 16:39:13 -06:00
|
|
|
},
|
2017-10-12 18:02:29 -05:00
|
|
|
|
2017-02-15 15:19:52 -06:00
|
|
|
/**
|
|
|
|
|
* Obtains current P2P ICE connection state.
|
|
|
|
|
* @return {string|null} ICE connection state or <tt>null</tt> if there's no
|
|
|
|
|
* P2P connection
|
|
|
|
|
*/
|
2017-04-11 14:40:03 -05:00
|
|
|
getP2PConnectionState() {
|
2019-07-02 14:11:56 +01:00
|
|
|
return room && room.getP2PConnectionState();
|
2017-02-15 15:19:52 -06:00
|
|
|
},
|
2017-10-12 18:02:29 -05:00
|
|
|
|
2017-02-15 15:19:52 -06:00
|
|
|
/**
|
|
|
|
|
* Starts P2P (for tests only)
|
|
|
|
|
* @private
|
|
|
|
|
*/
|
2017-04-11 14:40:03 -05:00
|
|
|
_startP2P() {
|
2017-02-15 15:19:52 -06:00
|
|
|
try {
|
2019-07-02 14:11:56 +01:00
|
|
|
room && room.startP2PSession();
|
2017-02-15 15:19:52 -06:00
|
|
|
} catch (error) {
|
2017-10-12 18:02:29 -05:00
|
|
|
logger.error('Start P2P failed', error);
|
2017-02-15 15:19:52 -06:00
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
},
|
2017-10-12 18:02:29 -05:00
|
|
|
|
2017-02-15 15:19:52 -06:00
|
|
|
/**
|
|
|
|
|
* Stops P2P (for tests only)
|
|
|
|
|
* @private
|
|
|
|
|
*/
|
2017-04-11 14:40:03 -05:00
|
|
|
_stopP2P() {
|
2017-02-15 15:19:52 -06:00
|
|
|
try {
|
2019-07-02 14:11:56 +01:00
|
|
|
room && room.stopP2PSession();
|
2017-02-15 15:19:52 -06:00
|
|
|
} catch (error) {
|
2017-10-12 18:02:29 -05:00
|
|
|
logger.error('Stop P2P failed', error);
|
2017-02-15 15:19:52 -06:00
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
},
|
2017-10-12 18:02:29 -05:00
|
|
|
|
2016-07-28 11:09:22 -05:00
|
|
|
/**
|
|
|
|
|
* Checks whether or not our connection is currently in interrupted and
|
|
|
|
|
* reconnect attempts are in progress.
|
|
|
|
|
*
|
|
|
|
|
* @returns {boolean} true if the connection is in interrupted state or
|
|
|
|
|
* false otherwise.
|
|
|
|
|
*/
|
2017-04-11 14:40:03 -05:00
|
|
|
isConnectionInterrupted() {
|
2019-07-02 14:11:56 +01:00
|
|
|
return room.isConnectionInterrupted();
|
2016-07-28 11:09:22 -05:00
|
|
|
},
|
2017-10-12 18:02:29 -05:00
|
|
|
|
2016-09-16 15:40:24 -05:00
|
|
|
/**
|
|
|
|
|
* Finds JitsiParticipant for given id.
|
|
|
|
|
*
|
|
|
|
|
* @param {string} id participant's identifier(MUC nickname).
|
|
|
|
|
*
|
|
|
|
|
* @returns {JitsiParticipant|null} participant instance for given id or
|
|
|
|
|
* null if not found.
|
|
|
|
|
*/
|
2017-04-11 14:40:03 -05:00
|
|
|
getParticipantById(id) {
|
2016-09-16 15:40:24 -05:00
|
|
|
return room ? room.getParticipantById(id) : null;
|
|
|
|
|
},
|
2017-10-12 18:02:29 -05:00
|
|
|
|
2017-04-11 14:40:03 -05:00
|
|
|
getMyUserId() {
|
2019-07-02 14:11:56 +01:00
|
|
|
return room && room.myUserId();
|
2016-01-06 16:39:13 -06:00
|
|
|
},
|
2017-10-12 18:02:29 -05:00
|
|
|
|
2016-01-19 13:32:29 -06:00
|
|
|
/**
|
2025-01-15 10:17:12 -06:00
|
|
|
* Will be filled with values only when config.testing.testMode is true.
|
2016-01-19 13:32:29 -06:00
|
|
|
* Its used by torture to check audio levels.
|
|
|
|
|
*/
|
|
|
|
|
audioLevelsMap: {},
|
2017-10-12 18:02:29 -05:00
|
|
|
|
2016-02-02 14:54:15 -06:00
|
|
|
/**
|
|
|
|
|
* Returns the stored audio level (stored only if config.debug is enabled)
|
|
|
|
|
* @param id the id for the user audio level to return (the id value is
|
|
|
|
|
* returned for the participant using getMyUserId() method)
|
|
|
|
|
*/
|
2017-04-11 14:40:03 -05:00
|
|
|
getPeerSSRCAudioLevel(id) {
|
2016-01-19 13:32:29 -06:00
|
|
|
return this.audioLevelsMap[id];
|
|
|
|
|
},
|
2017-10-12 18:02:29 -05:00
|
|
|
|
2016-01-14 13:50:10 -06:00
|
|
|
/**
|
2016-04-21 17:48:30 -05:00
|
|
|
* @return {number} the number of participants in the conference with at
|
|
|
|
|
* least one track.
|
2016-01-14 13:50:10 -06:00
|
|
|
*/
|
2016-04-21 17:48:30 -05:00
|
|
|
getNumberOfParticipantsWithTracks() {
|
2019-07-02 14:11:56 +01:00
|
|
|
return room.getParticipants()
|
2017-10-12 18:02:29 -05:00
|
|
|
.filter(p => p.getTracks().length > 0)
|
2016-04-21 17:48:30 -05:00
|
|
|
.length;
|
2016-01-14 13:50:10 -06:00
|
|
|
},
|
2017-10-12 18:02:29 -05:00
|
|
|
|
2016-02-04 12:41:23 -06:00
|
|
|
/**
|
|
|
|
|
* Returns the stats.
|
|
|
|
|
*/
|
|
|
|
|
getStats() {
|
2016-10-26 14:29:40 -05:00
|
|
|
return room.connectionQuality.getStats();
|
2016-02-04 12:41:23 -06:00
|
|
|
},
|
2017-10-12 18:02:29 -05:00
|
|
|
|
2016-01-14 13:50:10 -06:00
|
|
|
// end used by torture
|
|
|
|
|
|
2016-09-21 15:46:10 -05:00
|
|
|
/**
|
|
|
|
|
* Download logs, a function that can be called from console while
|
|
|
|
|
* debugging.
|
|
|
|
|
* @param filename (optional) specify target filename
|
|
|
|
|
*/
|
2017-04-11 14:40:03 -05:00
|
|
|
saveLogs(filename = 'meetlog.json') {
|
2016-09-21 15:46:10 -05:00
|
|
|
// this can be called from console and will not have reference to this
|
|
|
|
|
// that's why we reference the global var
|
2020-08-13 13:12:56 -05:00
|
|
|
const logs = APP.connection.getLogs();
|
2020-10-02 16:20:24 +03:00
|
|
|
|
|
|
|
|
downloadJSON(logs, filename);
|
2016-09-21 15:46:10 -05:00
|
|
|
},
|
|
|
|
|
|
2025-04-04 09:17:11 -05:00
|
|
|
/**
|
|
|
|
|
* Download app state, a function that can be called from console while debugging.
|
|
|
|
|
* @param filename (optional) specify target filename
|
|
|
|
|
*/
|
|
|
|
|
saveState(filename = 'meet-state.json') {
|
|
|
|
|
downloadJSON(APP.store.getState(), filename);
|
|
|
|
|
},
|
|
|
|
|
|
2016-03-11 04:49:13 -06:00
|
|
|
/**
|
|
|
|
|
* Exposes a Command(s) API on this instance. It is necessitated by (1) the
|
|
|
|
|
* desire to keep room private to this instance and (2) the need of other
|
|
|
|
|
* modules to send and receive commands to and from participants.
|
|
|
|
|
* Eventually, this instance remains in control with respect to the
|
|
|
|
|
* decision whether the Command(s) API of room (i.e. lib-jitsi-meet's
|
|
|
|
|
* JitsiConference) is to be used in the implementation of the Command(s)
|
|
|
|
|
* API of this instance.
|
|
|
|
|
*/
|
|
|
|
|
commands: {
|
2016-04-07 12:08:00 -05:00
|
|
|
/**
|
|
|
|
|
* Known custom conference commands.
|
|
|
|
|
*/
|
2016-06-13 16:11:44 -05:00
|
|
|
defaults: commands,
|
2017-10-12 18:02:29 -05:00
|
|
|
|
2016-03-11 04:49:13 -06:00
|
|
|
/**
|
|
|
|
|
* Receives notifications from other participants about commands aka
|
|
|
|
|
* custom events (sent by sendCommand or sendCommandOnce methods).
|
|
|
|
|
* @param command {String} the name of the command
|
|
|
|
|
* @param handler {Function} handler for the command
|
|
|
|
|
*/
|
2017-10-12 18:02:29 -05:00
|
|
|
addCommandListener() {
|
|
|
|
|
// eslint-disable-next-line prefer-rest-params
|
|
|
|
|
room.addCommandListener(...arguments);
|
2016-03-11 04:49:13 -06:00
|
|
|
},
|
2017-10-12 18:02:29 -05:00
|
|
|
|
2016-03-11 04:49:13 -06:00
|
|
|
/**
|
|
|
|
|
* Removes command.
|
|
|
|
|
* @param name {String} the name of the command.
|
|
|
|
|
*/
|
2017-10-12 18:02:29 -05:00
|
|
|
removeCommand() {
|
|
|
|
|
// eslint-disable-next-line prefer-rest-params
|
|
|
|
|
room.removeCommand(...arguments);
|
2016-03-11 04:49:13 -06:00
|
|
|
},
|
2017-10-12 18:02:29 -05:00
|
|
|
|
2016-03-11 04:49:13 -06:00
|
|
|
/**
|
|
|
|
|
* Sends command.
|
|
|
|
|
* @param name {String} the name of the command.
|
|
|
|
|
* @param values {Object} with keys and values that will be sent.
|
|
|
|
|
*/
|
2017-10-12 18:02:29 -05:00
|
|
|
sendCommand() {
|
|
|
|
|
// eslint-disable-next-line prefer-rest-params
|
|
|
|
|
room.sendCommand(...arguments);
|
2016-03-11 04:49:13 -06:00
|
|
|
},
|
2017-10-12 18:02:29 -05:00
|
|
|
|
2016-03-11 04:49:13 -06:00
|
|
|
/**
|
|
|
|
|
* Sends command one time.
|
|
|
|
|
* @param name {String} the name of the command.
|
|
|
|
|
* @param values {Object} with keys and values that will be sent.
|
|
|
|
|
*/
|
2017-10-12 18:02:29 -05:00
|
|
|
sendCommandOnce() {
|
|
|
|
|
// eslint-disable-next-line prefer-rest-params
|
|
|
|
|
room.sendCommandOnce(...arguments);
|
2016-04-07 12:08:00 -05:00
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2021-09-14 17:31:30 +02:00
|
|
|
/**
|
|
|
|
|
* Used by the Breakout Rooms feature to join a breakout room or go back to the main room.
|
|
|
|
|
*/
|
2021-12-10 14:40:41 +01:00
|
|
|
async joinRoom(roomName, options) {
|
2023-07-15 17:33:26 -05:00
|
|
|
APP.store.dispatch(conferenceWillInit());
|
2021-11-30 16:10:49 +01:00
|
|
|
|
2021-12-10 14:40:41 +01:00
|
|
|
// Restore initial state.
|
2021-11-30 16:10:49 +01:00
|
|
|
this._localTracksInitialized = false;
|
2021-09-14 17:31:30 +02:00
|
|
|
this.roomName = roomName;
|
|
|
|
|
|
2021-12-10 14:40:41 +01:00
|
|
|
const { tryCreateLocalTracks, errors } = this.createInitialLocalTracks(options);
|
2021-09-14 17:31:30 +02:00
|
|
|
const localTracks = await tryCreateLocalTracks;
|
|
|
|
|
|
2024-05-07 15:32:45 -05:00
|
|
|
APP.store.dispatch(displayErrorsForCreateInitialLocalTracks(errors));
|
2021-09-14 17:31:30 +02:00
|
|
|
localTracks.forEach(track => {
|
|
|
|
|
if ((track.isAudioTrack() && this.isLocalAudioMuted())
|
|
|
|
|
|| (track.isVideoTrack() && this.isLocalVideoMuted())) {
|
|
|
|
|
track.mute();
|
|
|
|
|
}
|
|
|
|
|
});
|
2021-11-30 10:12:59 +01:00
|
|
|
this._createRoom(localTracks);
|
2021-09-14 17:31:30 +02:00
|
|
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
2022-12-13 08:26:22 -06:00
|
|
|
new ConferenceConnector(resolve, reject, this).connect();
|
2021-09-14 17:31:30 +02:00
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
|
2021-11-30 10:12:59 +01:00
|
|
|
_createRoom(localTracks) {
|
2023-07-15 17:33:26 -05:00
|
|
|
room = APP.connection.initJitsiConference(APP.conference.roomName, this._getConferenceOptions());
|
2019-02-06 19:19:02 -08:00
|
|
|
|
2021-10-05 12:40:19 -04:00
|
|
|
// Filter out the tracks that are muted (except on Safari).
|
2024-02-13 10:20:52 -06:00
|
|
|
let tracks = localTracks;
|
|
|
|
|
|
|
|
|
|
if (!browser.isWebKitBased()) {
|
|
|
|
|
const mutedTrackTypes = [];
|
|
|
|
|
|
|
|
|
|
tracks = localTracks.filter(track => {
|
|
|
|
|
if (!track.isMuted()) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (track.getVideoType() !== VIDEO_TYPE.DESKTOP) {
|
|
|
|
|
mutedTrackTypes.push(track.getType());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
});
|
|
|
|
|
APP.store.dispatch(gumPending(mutedTrackTypes, IGUMPendingState.NONE));
|
|
|
|
|
}
|
2021-03-05 10:18:34 -05:00
|
|
|
|
2016-04-07 12:08:00 -05:00
|
|
|
this._room = room; // FIXME do not use this
|
|
|
|
|
|
2021-09-14 16:57:05 -05:00
|
|
|
APP.store.dispatch(_conferenceWillJoin(room));
|
|
|
|
|
|
2024-07-10 09:54:41 +03:00
|
|
|
this._setLocalAudioVideoStreams(tracks);
|
|
|
|
|
|
2017-10-13 14:31:05 -05:00
|
|
|
sendLocalParticipant(APP.store, room);
|
2016-04-07 12:08:00 -05:00
|
|
|
|
|
|
|
|
this._setupListeners();
|
2016-03-11 04:49:13 -06:00
|
|
|
},
|
|
|
|
|
|
2016-06-13 14:49:00 +03:00
|
|
|
/**
|
|
|
|
|
* Sets local video and audio streams.
|
|
|
|
|
* @param {JitsiLocalTrack[]} tracks=[]
|
|
|
|
|
* @returns {Promise[]}
|
|
|
|
|
* @private
|
|
|
|
|
*/
|
|
|
|
|
_setLocalAudioVideoStreams(tracks = []) {
|
2023-05-18 14:16:37 -05:00
|
|
|
const { dispatch } = APP.store;
|
|
|
|
|
const pendingGUMDevicesToRemove = [];
|
2021-09-13 12:33:04 -05:00
|
|
|
const promises = tracks.map(track => {
|
2016-06-13 14:49:00 +03:00
|
|
|
if (track.isAudioTrack()) {
|
2023-05-18 14:16:37 -05:00
|
|
|
pendingGUMDevicesToRemove.push(MEDIA_TYPE.AUDIO);
|
|
|
|
|
|
2016-06-13 14:49:00 +03:00
|
|
|
return this.useAudioStream(track);
|
|
|
|
|
} else if (track.isVideoTrack()) {
|
2021-03-05 12:17:39 -06:00
|
|
|
logger.debug(`_setLocalAudioVideoStreams is calling useVideoStream with track: ${track}`);
|
2023-05-18 14:16:37 -05:00
|
|
|
pendingGUMDevicesToRemove.push(MEDIA_TYPE.VIDEO);
|
2021-03-05 12:17:39 -06:00
|
|
|
|
2016-06-13 14:49:00 +03:00
|
|
|
return this.useVideoStream(track);
|
|
|
|
|
}
|
2021-03-05 12:17:39 -06:00
|
|
|
|
2021-09-13 12:33:04 -05:00
|
|
|
logger.error('Ignored not an audio nor a video track: ', track);
|
2017-10-12 18:02:29 -05:00
|
|
|
|
|
|
|
|
return Promise.resolve();
|
|
|
|
|
|
2016-06-13 14:49:00 +03:00
|
|
|
});
|
2021-09-13 12:33:04 -05:00
|
|
|
|
|
|
|
|
return Promise.allSettled(promises).then(() => {
|
2023-05-18 14:16:37 -05:00
|
|
|
if (pendingGUMDevicesToRemove.length > 0) {
|
|
|
|
|
dispatch(gumPending(pendingGUMDevicesToRemove, IGUMPendingState.NONE));
|
|
|
|
|
}
|
|
|
|
|
|
2021-09-13 12:33:04 -05:00
|
|
|
this._localTracksInitialized = true;
|
|
|
|
|
logger.log(`Initialized with ${tracks.length} local tracks`);
|
|
|
|
|
});
|
2016-06-13 14:49:00 +03:00
|
|
|
},
|
|
|
|
|
|
2016-01-06 16:39:13 -06:00
|
|
|
_getConferenceOptions() {
|
2021-08-04 12:56:07 +02:00
|
|
|
const options = getConferenceOptions(APP.store.getState());
|
|
|
|
|
|
|
|
|
|
options.createVADProcessor = createRnnoiseProcessor;
|
|
|
|
|
|
|
|
|
|
return options;
|
2019-07-08 11:54:54 +01:00
|
|
|
},
|
|
|
|
|
|
2017-04-11 14:40:03 -05:00
|
|
|
/**
|
|
|
|
|
* Start using provided video stream.
|
|
|
|
|
* Stops previous video stream.
|
2020-06-26 11:54:12 +03:00
|
|
|
* @param {JitsiLocalTrack} newTrack - new track to use or null
|
2017-04-11 14:40:03 -05:00
|
|
|
* @returns {Promise}
|
|
|
|
|
*/
|
2020-06-26 11:54:12 +03:00
|
|
|
useVideoStream(newTrack) {
|
2021-03-05 12:17:39 -06:00
|
|
|
logger.debug(`useVideoStream: ${newTrack}`);
|
|
|
|
|
|
2018-08-31 13:02:04 -07:00
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
_replaceLocalVideoTrackQueue.enqueue(onFinish => {
|
2024-04-12 12:18:41 -05:00
|
|
|
const state = APP.store.getState();
|
2022-03-15 13:24:49 -04:00
|
|
|
const oldTrack = getLocalJitsiVideoTrack(state);
|
2020-06-26 11:54:12 +03:00
|
|
|
|
2021-09-13 12:33:04 -05:00
|
|
|
logger.debug(`useVideoStream: Replacing ${oldTrack} with ${newTrack}`);
|
2020-06-26 11:54:12 +03:00
|
|
|
|
2023-03-13 21:09:46 -05:00
|
|
|
if (oldTrack === newTrack || (!oldTrack && !newTrack)) {
|
2021-09-13 12:33:04 -05:00
|
|
|
resolve();
|
|
|
|
|
onFinish();
|
2021-03-05 12:17:39 -06:00
|
|
|
|
2021-09-13 12:33:04 -05:00
|
|
|
return;
|
2020-06-26 11:54:12 +03:00
|
|
|
}
|
|
|
|
|
|
2022-11-18 16:20:29 -05:00
|
|
|
// Add the track to the conference if there is no existing track, replace it otherwise.
|
|
|
|
|
const trackAction = oldTrack
|
|
|
|
|
? replaceLocalTrack(oldTrack, newTrack, room)
|
|
|
|
|
: addLocalTrack(newTrack);
|
2022-03-15 13:24:49 -04:00
|
|
|
|
2022-11-18 16:20:29 -05:00
|
|
|
APP.store.dispatch(trackAction)
|
2018-08-31 13:02:04 -07:00
|
|
|
.then(() => {
|
2021-05-04 15:57:34 +03:00
|
|
|
this.setVideoMuteStatus();
|
2018-08-31 13:02:04 -07:00
|
|
|
})
|
|
|
|
|
.then(resolve)
|
2021-03-05 12:17:39 -06:00
|
|
|
.catch(error => {
|
|
|
|
|
logger.error(`useVideoStream failed: ${error}`);
|
|
|
|
|
reject(error);
|
|
|
|
|
})
|
2018-08-31 13:02:04 -07:00
|
|
|
.then(onFinish);
|
2017-01-19 12:46:11 -06:00
|
|
|
});
|
2018-08-31 13:02:04 -07:00
|
|
|
});
|
2016-02-09 12:19:43 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Start using provided audio stream.
|
|
|
|
|
* Stops previous audio stream.
|
2020-06-26 11:54:12 +03:00
|
|
|
* @param {JitsiLocalTrack} newTrack - new track to use or null
|
2016-02-16 15:33:53 +02:00
|
|
|
* @returns {Promise}
|
2016-02-09 12:19:43 +02:00
|
|
|
*/
|
2020-06-26 11:54:12 +03:00
|
|
|
useAudioStream(newTrack) {
|
2018-08-31 13:02:04 -07:00
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
_replaceLocalAudioTrackQueue.enqueue(onFinish => {
|
2021-09-13 12:33:04 -05:00
|
|
|
const oldTrack = getLocalJitsiAudioTrack(APP.store.getState());
|
2020-06-26 11:54:12 +03:00
|
|
|
|
2021-09-13 12:33:04 -05:00
|
|
|
if (oldTrack === newTrack) {
|
|
|
|
|
resolve();
|
|
|
|
|
onFinish();
|
2020-06-26 11:54:12 +03:00
|
|
|
|
2021-09-13 12:33:04 -05:00
|
|
|
return;
|
2020-06-26 11:54:12 +03:00
|
|
|
}
|
|
|
|
|
|
2024-07-23 16:57:44 -05:00
|
|
|
APP.store.dispatch(replaceLocalTrack(oldTrack, newTrack, room))
|
2018-08-31 13:02:04 -07:00
|
|
|
.then(() => {
|
2023-10-09 15:37:50 -05:00
|
|
|
this.updateAudioIconEnabled();
|
2018-08-31 13:02:04 -07:00
|
|
|
})
|
|
|
|
|
.then(resolve)
|
|
|
|
|
.catch(reject)
|
|
|
|
|
.then(onFinish);
|
2017-01-19 12:46:11 -06:00
|
|
|
});
|
2018-08-31 13:02:04 -07:00
|
|
|
});
|
2016-02-09 12:19:43 +02:00
|
|
|
},
|
|
|
|
|
|
2017-04-05 17:14:26 +02:00
|
|
|
/**
|
|
|
|
|
* Returns whether or not the conference is currently in audio only mode.
|
|
|
|
|
*
|
|
|
|
|
* @returns {boolean}
|
|
|
|
|
*/
|
|
|
|
|
isAudioOnly() {
|
2019-07-31 14:47:52 +02:00
|
|
|
return Boolean(APP.store.getState()['features/base/audio-only'].enabled);
|
2017-04-05 17:14:26 +02:00
|
|
|
},
|
2017-03-29 10:43:30 -07:00
|
|
|
|
2017-06-20 09:52:44 -05:00
|
|
|
/**
|
|
|
|
|
* This fields stores a handler which will create a Promise which turns off
|
|
|
|
|
* the screen sharing and restores the previous video state (was there
|
|
|
|
|
* any video, before switching to screen sharing ? was it muted ?).
|
|
|
|
|
*
|
|
|
|
|
* Once called this fields is cleared to <tt>null</tt>.
|
|
|
|
|
* @type {Function|null}
|
|
|
|
|
*/
|
|
|
|
|
_untoggleScreenSharing: null,
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Creates a Promise which turns off the screen sharing and restores
|
|
|
|
|
* the previous state described by the arguments.
|
|
|
|
|
*
|
|
|
|
|
* This method is bound to the appropriate values, after switching to screen
|
|
|
|
|
* sharing and stored in {@link _untoggleScreenSharing}.
|
|
|
|
|
*
|
|
|
|
|
* @param {boolean} didHaveVideo indicates if there was a camera video being
|
|
|
|
|
* used, before switching to screen sharing.
|
2021-09-22 15:11:43 +03:00
|
|
|
* @param {boolean} ignoreDidHaveVideo indicates if the camera video should be
|
|
|
|
|
* ignored when switching screen sharing off.
|
2017-06-20 09:52:44 -05:00
|
|
|
* @return {Promise} resolved after the screen sharing is turned off, or
|
|
|
|
|
* rejected with some error (no idea what kind of error, possible GUM error)
|
|
|
|
|
* in case it fails.
|
|
|
|
|
* @private
|
|
|
|
|
*/
|
2021-09-22 15:11:43 +03:00
|
|
|
async _turnScreenSharingOff(didHaveVideo, ignoreDidHaveVideo) {
|
2017-06-20 09:52:44 -05:00
|
|
|
this._untoggleScreenSharing = null;
|
2017-10-19 19:38:55 -05:00
|
|
|
|
2020-11-13 22:09:25 -06:00
|
|
|
APP.store.dispatch(stopReceiver());
|
2017-10-19 19:38:55 -05:00
|
|
|
|
2019-01-26 12:53:11 -08:00
|
|
|
this._stopProxyConnection();
|
2021-07-26 14:38:56 +03:00
|
|
|
|
2022-01-28 11:11:35 +02:00
|
|
|
APP.store.dispatch(toggleScreenshotCaptureSummary(false));
|
2021-10-14 13:17:56 +03:00
|
|
|
const tracks = APP.store.getState()['features/base/tracks'];
|
2021-11-08 10:55:28 +02:00
|
|
|
const duration = getLocalVideoTrack(tracks)?.jitsiTrack.getDuration() ?? 0;
|
2019-01-26 12:53:11 -08:00
|
|
|
|
2020-03-26 14:17:44 +02:00
|
|
|
// If system audio was also shared stop the AudioMixerEffect and dispose of the desktop audio track.
|
|
|
|
|
if (this._mixerEffect) {
|
2021-09-13 12:33:04 -05:00
|
|
|
const localAudio = getLocalJitsiAudioTrack(APP.store.getState());
|
|
|
|
|
|
|
|
|
|
await localAudio.setEffect(undefined);
|
2020-03-26 14:17:44 +02:00
|
|
|
await this._desktopAudioStream.dispose();
|
|
|
|
|
this._mixerEffect = undefined;
|
|
|
|
|
this._desktopAudioStream = undefined;
|
|
|
|
|
|
|
|
|
|
// In case there was no local audio when screen sharing was started the fact that we set the audio stream to
|
|
|
|
|
// null will take care of the desktop audio stream cleanup.
|
|
|
|
|
} else if (this._desktopAudioStream) {
|
2022-04-04 11:19:33 -04:00
|
|
|
await room.replaceTrack(this._desktopAudioStream, null);
|
|
|
|
|
this._desktopAudioStream.dispose();
|
2020-03-26 14:17:44 +02:00
|
|
|
this._desktopAudioStream = undefined;
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-12 10:37:39 +03:00
|
|
|
APP.store.dispatch(setScreenAudioShareState(false));
|
2022-11-08 14:15:49 -05:00
|
|
|
let promise;
|
2021-04-12 10:37:39 +03:00
|
|
|
|
2022-03-29 10:37:58 -04:00
|
|
|
if (didHaveVideo && !ignoreDidHaveVideo) {
|
2022-11-08 14:15:49 -05:00
|
|
|
promise = createLocalTracksF({ devices: [ 'video' ] })
|
2022-03-29 10:37:58 -04:00
|
|
|
.then(([ stream ]) => {
|
|
|
|
|
logger.debug(`_turnScreenSharingOff using ${stream} for useVideoStream`);
|
2021-03-05 12:17:39 -06:00
|
|
|
|
2022-03-29 10:37:58 -04:00
|
|
|
return this.useVideoStream(stream);
|
|
|
|
|
})
|
|
|
|
|
.catch(error => {
|
|
|
|
|
logger.error('failed to switch back to local video', error);
|
|
|
|
|
|
|
|
|
|
return this.useVideoStream(null).then(() =>
|
2017-10-12 18:02:29 -05:00
|
|
|
|
2022-03-29 10:37:58 -04:00
|
|
|
// Still fail with the original err
|
|
|
|
|
Promise.reject(error)
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
} else {
|
2022-11-08 14:15:49 -05:00
|
|
|
promise = this.useVideoStream(null);
|
2022-03-29 10:37:58 -04:00
|
|
|
}
|
2019-09-19 16:28:57 +03:00
|
|
|
|
2017-06-20 09:52:44 -05:00
|
|
|
return promise.then(
|
|
|
|
|
() => {
|
2021-10-14 13:17:56 +03:00
|
|
|
sendAnalytics(createScreenSharingEvent('stopped',
|
2021-11-08 10:55:28 +02:00
|
|
|
duration === 0 ? null : duration));
|
2020-08-04 17:11:58 -04:00
|
|
|
logger.info('Screen sharing stopped.');
|
2017-06-20 09:52:44 -05:00
|
|
|
},
|
|
|
|
|
error => {
|
2021-03-05 12:17:39 -06:00
|
|
|
logger.error(`_turnScreenSharingOff failed: ${error}`);
|
|
|
|
|
|
2017-06-20 09:52:44 -05:00
|
|
|
throw error;
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
|
2017-06-29 19:43:35 +02:00
|
|
|
/**
|
|
|
|
|
* Creates desktop (screensharing) {@link JitsiLocalTrack}
|
2019-01-26 12:53:11 -08:00
|
|
|
*
|
2017-06-29 19:43:35 +02:00
|
|
|
* @return {Promise.<JitsiLocalTrack>} - A Promise resolved with
|
|
|
|
|
* {@link JitsiLocalTrack} for the screensharing or rejected with
|
|
|
|
|
* {@link JitsiTrackError}.
|
|
|
|
|
*
|
|
|
|
|
* @private
|
|
|
|
|
*/
|
2025-01-15 10:15:06 -06:00
|
|
|
_createDesktopTrack() {
|
2019-11-27 12:36:44 -05:00
|
|
|
const didHaveVideo = !this.isLocalVideoMuted();
|
2017-06-29 19:43:35 +02:00
|
|
|
|
2025-01-15 10:15:06 -06:00
|
|
|
const getDesktopStreamPromise = createLocalTracksF({
|
|
|
|
|
desktopSharingSourceDevice: config._desktopSharingSourceDevice,
|
|
|
|
|
devices: [ 'desktop' ]
|
|
|
|
|
});
|
2019-01-26 12:53:11 -08:00
|
|
|
|
2020-03-26 14:17:44 +02:00
|
|
|
return getDesktopStreamPromise.then(desktopStreams => {
|
2017-06-29 19:43:35 +02:00
|
|
|
// Stores the "untoggle" handler which remembers whether was
|
|
|
|
|
// there any video before and whether was it muted.
|
|
|
|
|
this._untoggleScreenSharing
|
2019-11-26 05:57:03 -05:00
|
|
|
= this._turnScreenSharingOff.bind(this, didHaveVideo);
|
2020-03-26 14:17:44 +02:00
|
|
|
|
2021-12-10 13:33:16 +08:00
|
|
|
const desktopAudioStream = desktopStreams.find(stream => stream.getType() === MEDIA_TYPE.AUDIO);
|
2021-04-12 10:37:39 +03:00
|
|
|
|
2021-12-10 13:33:16 +08:00
|
|
|
if (desktopAudioStream) {
|
|
|
|
|
desktopAudioStream.on(
|
2021-04-12 10:37:39 +03:00
|
|
|
JitsiTrackEvents.LOCAL_TRACK_STOPPED,
|
|
|
|
|
() => {
|
2025-06-30 10:59:28 +02:00
|
|
|
logger.debug('Local screensharing audio track stopped.');
|
2021-04-12 10:37:39 +03:00
|
|
|
|
|
|
|
|
// Handle case where screen share was stopped from the browsers 'screen share in progress'
|
|
|
|
|
// window. If audio screen sharing is stopped via the normal UX flow this point shouldn't
|
|
|
|
|
// be reached.
|
|
|
|
|
isScreenAudioShared(APP.store.getState())
|
|
|
|
|
&& this._untoggleScreenSharing
|
|
|
|
|
&& this._untoggleScreenSharing();
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
}
|
2020-03-26 14:17:44 +02:00
|
|
|
|
|
|
|
|
return desktopStreams;
|
2017-06-29 19:43:35 +02:00
|
|
|
}, error => {
|
|
|
|
|
throw error;
|
|
|
|
|
});
|
|
|
|
|
},
|
2016-03-04 15:21:07 -06:00
|
|
|
|
2016-01-15 16:59:35 +02:00
|
|
|
/**
|
|
|
|
|
* Setup interaction between conference and UI.
|
|
|
|
|
*/
|
2017-04-11 14:40:03 -05:00
|
|
|
_setupListeners() {
|
2016-01-06 16:39:13 -06:00
|
|
|
// add local streams when joined to the conference
|
2017-10-10 18:31:40 -05:00
|
|
|
room.on(JitsiConferenceEvents.CONFERENCE_JOINED, () => {
|
2017-11-16 14:54:49 -08:00
|
|
|
this._onConferenceJoined();
|
2016-01-06 16:39:13 -06:00
|
|
|
});
|
2022-04-05 21:13:39 -05:00
|
|
|
room.on(
|
|
|
|
|
JitsiConferenceEvents.CONFERENCE_JOIN_IN_PROGRESS,
|
|
|
|
|
() => APP.store.dispatch(conferenceJoinInProgress(room)));
|
2016-01-06 16:39:13 -06:00
|
|
|
|
2017-03-07 10:50:17 -06:00
|
|
|
room.on(
|
2017-10-10 18:31:40 -05:00
|
|
|
JitsiConferenceEvents.CONFERENCE_LEFT,
|
2020-01-13 19:12:25 +02:00
|
|
|
(...args) => {
|
|
|
|
|
APP.store.dispatch(conferenceTimestampChanged(0));
|
|
|
|
|
APP.store.dispatch(conferenceLeft(room, ...args));
|
|
|
|
|
});
|
2017-02-27 15:42:28 -06:00
|
|
|
|
2021-02-03 12:28:39 +02:00
|
|
|
room.on(
|
|
|
|
|
JitsiConferenceEvents.CONFERENCE_UNIQUE_ID_SET,
|
2021-06-02 12:27:15 +03:00
|
|
|
(...args) => {
|
|
|
|
|
// Preserve the sessionId so that the value is accessible even after room
|
|
|
|
|
// is disconnected.
|
|
|
|
|
room.sessionId = room.getMeetingUniqueId();
|
|
|
|
|
APP.store.dispatch(conferenceUniqueIdSet(room, ...args));
|
|
|
|
|
});
|
2021-02-03 12:28:39 +02:00
|
|
|
|
2023-07-24 23:22:12 -05:00
|
|
|
// we want to ignore this event in case of tokenAuthUrl config
|
|
|
|
|
// we are deprecating this and at some point will get rid of it
|
|
|
|
|
if (!config.tokenAuthUrl) {
|
|
|
|
|
room.on(
|
|
|
|
|
JitsiConferenceEvents.AUTH_STATUS_CHANGED,
|
|
|
|
|
(authEnabled, authLogin) =>
|
|
|
|
|
APP.store.dispatch(authStatusChanged(authEnabled, authLogin)));
|
|
|
|
|
}
|
2016-02-25 14:32:52 +02:00
|
|
|
|
2020-11-13 22:09:25 -06:00
|
|
|
room.on(JitsiConferenceEvents.PARTCIPANT_FEATURES_CHANGED, user => {
|
|
|
|
|
APP.store.dispatch(updateRemoteParticipantFeatures(user));
|
|
|
|
|
});
|
2017-10-10 18:31:40 -05:00
|
|
|
room.on(JitsiConferenceEvents.USER_JOINED, (id, user) => {
|
2022-02-17 16:25:31 -06:00
|
|
|
if (config.iAmRecorder && user.isHiddenFromRecorder()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-16 10:11:06 -05:00
|
|
|
// The logic shared between RN and web.
|
|
|
|
|
commonUserJoinedHandling(APP.store, room, user);
|
2018-07-26 18:33:40 +02:00
|
|
|
|
2017-10-12 18:02:29 -05:00
|
|
|
if (user.isHidden()) {
|
2016-04-26 16:38:07 -05:00
|
|
|
return;
|
2017-10-12 18:02:29 -05:00
|
|
|
}
|
2016-04-26 16:38:07 -05:00
|
|
|
|
2020-11-13 22:09:25 -06:00
|
|
|
APP.store.dispatch(updateRemoteParticipantFeatures(user));
|
2025-10-21 18:53:08 -05:00
|
|
|
logger.log(`USER ${id} connected`);
|
2016-09-16 15:17:00 -05:00
|
|
|
APP.UI.addUser(user);
|
2016-01-06 16:39:13 -06:00
|
|
|
});
|
2017-08-29 08:08:16 -07:00
|
|
|
|
2017-10-10 18:31:40 -05:00
|
|
|
room.on(JitsiConferenceEvents.USER_LEFT, (id, user) => {
|
2018-08-16 10:11:06 -05:00
|
|
|
// The logic shared between RN and web.
|
|
|
|
|
commonUserLeftHandling(APP.store, room, user);
|
2018-07-26 18:33:40 +02:00
|
|
|
|
2018-08-16 10:11:06 -05:00
|
|
|
if (user.isHidden()) {
|
2018-02-22 11:33:10 -08:00
|
|
|
return;
|
|
|
|
|
}
|
2018-07-26 18:33:40 +02:00
|
|
|
|
2018-07-23 20:11:47 +02:00
|
|
|
logger.log(`USER ${id} LEFT:`, user);
|
2016-01-06 16:39:13 -06:00
|
|
|
});
|
|
|
|
|
|
2017-10-10 18:31:40 -05:00
|
|
|
room.on(JitsiConferenceEvents.USER_STATUS_CHANGED, (id, status) => {
|
2017-07-31 16:33:22 -07:00
|
|
|
APP.store.dispatch(participantPresenceChanged(id, status));
|
|
|
|
|
|
2017-10-12 18:02:29 -05:00
|
|
|
const user = room.getParticipantById(id);
|
|
|
|
|
|
2017-05-19 10:12:24 -05:00
|
|
|
if (user) {
|
|
|
|
|
APP.UI.updateUserStatus(user, status);
|
|
|
|
|
}
|
|
|
|
|
});
|
2016-01-06 16:39:13 -06:00
|
|
|
|
2017-10-10 18:31:40 -05:00
|
|
|
room.on(JitsiConferenceEvents.USER_ROLE_CHANGED, (id, role) => {
|
2016-01-06 16:39:13 -06:00
|
|
|
if (this.isLocalId(id)) {
|
2016-11-11 09:00:54 -06:00
|
|
|
logger.info(`My role changed, new role: ${role}`);
|
2017-04-10 14:53:30 -07:00
|
|
|
|
2022-03-03 20:29:38 +03:00
|
|
|
if (role === 'moderator') {
|
|
|
|
|
APP.store.dispatch(maybeSetLobbyChatMessageListener());
|
|
|
|
|
}
|
|
|
|
|
|
2017-04-10 14:53:30 -07:00
|
|
|
APP.store.dispatch(localParticipantRoleChanged(role));
|
2016-01-06 16:39:13 -06:00
|
|
|
} else {
|
2017-04-10 14:53:30 -07:00
|
|
|
APP.store.dispatch(participantRoleChanged(id, role));
|
2016-01-06 16:39:13 -06:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2017-10-12 18:02:29 -05:00
|
|
|
room.on(JitsiConferenceEvents.TRACK_ADDED, track => {
|
|
|
|
|
if (!track || track.isLocal()) {
|
2016-01-06 16:39:13 -06:00
|
|
|
return;
|
2017-10-12 18:02:29 -05:00
|
|
|
}
|
2016-01-29 16:06:54 -06:00
|
|
|
|
2022-02-17 16:25:31 -06:00
|
|
|
if (config.iAmRecorder) {
|
|
|
|
|
const participant = room.getParticipantById(track.getParticipantId());
|
|
|
|
|
|
|
|
|
|
if (participant.isHiddenFromRecorder()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-06-20 11:30:54 -05:00
|
|
|
APP.store.dispatch(trackAdded(track));
|
2016-01-06 16:39:13 -06:00
|
|
|
});
|
|
|
|
|
|
2017-10-12 18:02:29 -05:00
|
|
|
room.on(JitsiConferenceEvents.TRACK_REMOVED, track => {
|
|
|
|
|
if (!track || track.isLocal()) {
|
2016-02-23 16:47:55 -06:00
|
|
|
return;
|
2017-10-12 18:02:29 -05:00
|
|
|
}
|
2016-02-23 16:47:55 -06:00
|
|
|
|
2017-06-20 11:30:54 -05:00
|
|
|
APP.store.dispatch(trackRemoved(track));
|
2016-01-06 16:39:13 -06:00
|
|
|
});
|
|
|
|
|
|
2017-10-10 18:31:40 -05:00
|
|
|
room.on(JitsiConferenceEvents.TRACK_AUDIO_LEVEL_CHANGED, (id, lvl) => {
|
2021-09-13 12:33:04 -05:00
|
|
|
const localAudio = getLocalJitsiAudioTrack(APP.store.getState());
|
2017-10-12 18:02:29 -05:00
|
|
|
let newLvl = lvl;
|
|
|
|
|
|
2022-01-21 10:07:55 +02:00
|
|
|
if (this.isLocalId(id)) {
|
|
|
|
|
APP.store.dispatch(localParticipantAudioLevelChanged(lvl));
|
|
|
|
|
}
|
|
|
|
|
|
2021-09-13 12:33:04 -05:00
|
|
|
if (this.isLocalId(id) && localAudio?.isMuted()) {
|
2017-10-12 18:02:29 -05:00
|
|
|
newLvl = 0;
|
2016-01-06 16:39:13 -06:00
|
|
|
}
|
2016-01-19 13:32:29 -06:00
|
|
|
|
2025-01-15 10:17:12 -06:00
|
|
|
if (config.testing?.testMode) {
|
2017-10-12 18:02:29 -05:00
|
|
|
this.audioLevelsMap[id] = newLvl;
|
2025-01-15 10:59:33 -06:00
|
|
|
if (config.testing?.debugAudioLevels) {
|
2017-10-12 18:02:29 -05:00
|
|
|
logger.log(`AudioLevel:${id}/${newLvl}`);
|
|
|
|
|
}
|
2016-02-09 16:49:46 -06:00
|
|
|
}
|
2016-01-19 13:32:29 -06:00
|
|
|
|
2017-10-12 18:02:29 -05:00
|
|
|
APP.UI.setAudioLevel(id, newLvl);
|
2016-01-06 16:39:13 -06:00
|
|
|
});
|
|
|
|
|
|
2019-07-02 12:59:25 +01:00
|
|
|
room.on(JitsiConferenceEvents.TRACK_MUTE_CHANGED, (track, participantThatMutedUs) => {
|
2019-06-17 16:00:09 +02:00
|
|
|
if (participantThatMutedUs) {
|
2021-02-24 22:45:07 +01:00
|
|
|
APP.store.dispatch(participantMutedUs(participantThatMutedUs, track));
|
2019-06-17 16:00:09 +02:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2021-12-09 08:48:55 -05:00
|
|
|
room.on(JitsiConferenceEvents.TRACK_UNMUTE_REJECTED, track => APP.store.dispatch(destroyLocalTracks(track)));
|
|
|
|
|
|
2019-02-17 14:40:24 +00:00
|
|
|
room.on(JitsiConferenceEvents.SUBJECT_CHANGED,
|
2019-03-12 17:45:53 +00:00
|
|
|
subject => APP.store.dispatch(conferenceSubjectChanged(subject)));
|
2016-09-08 20:19:18 -05:00
|
|
|
|
2016-01-26 15:26:28 -06:00
|
|
|
room.on(
|
2017-10-10 18:31:40 -05:00
|
|
|
JitsiConferenceEvents.LAST_N_ENDPOINTS_CHANGED,
|
2017-10-02 18:08:07 -05:00
|
|
|
(leavingIds, enteringIds) =>
|
|
|
|
|
APP.UI.handleLastNEndpoints(leavingIds, enteringIds));
|
2017-03-21 12:14:13 -05:00
|
|
|
|
2017-08-09 12:40:03 -07:00
|
|
|
room.on(
|
2017-10-10 18:31:40 -05:00
|
|
|
JitsiConferenceEvents.P2P_STATUS,
|
2017-10-12 18:02:29 -05:00
|
|
|
(jitsiConference, p2p) =>
|
|
|
|
|
APP.store.dispatch(p2pStatusChanged(p2p)));
|
2017-08-09 12:40:03 -07:00
|
|
|
|
Associate remote participant w/ JitsiConference (_UPDATED)
The commit message of "Associate remote participant w/ JitsiConference
(_JOINED)" explains the motivation for this commit.
Practically, _JOINED and _LEFT combined with "Remove remote participants
who are no longer of interest" should alleviate the problem with
multiplying remote participants to an acceptable level of annoyance.
Technically though, a remote participant cannot be identified by an ID
only. The ID is (somewhat) "unique" in the context of a single
JitsiConference instance. So in order to not have to scratch our heads
over an obscure corner, racing case, it's better to always identify
remote participants by the pair id-conference. Unfortunately, that's a
bit of a high order given the existing source code. So I've implemented
the cases which are the easiest so that new source code written with
participantUpdated is more likely to identify a remote participant with
the pair id-conference.
Additionally, the commit "Reduce direct read access to the
features/base/participants redux state" brings more control back to the
functions of the feature base/participants so that one day we can (if we
choose to) do something like, for example:
If getParticipants is called with a conference, it returns the
participants from features/base/participants who are associated with the
specified conference. If no conference is specified in the function
call, then default to the conference which is the primary focus of the
app at the time of the function call. Added to the above, this should
allow us to further reduce the cases in which we're identifying remote
participants by id only and get us even closer to a more "predictable"
behavior in corner, racing cases.
2018-05-22 16:47:43 -05:00
|
|
|
room.on(
|
|
|
|
|
JitsiConferenceEvents.DOMINANT_SPEAKER_CHANGED,
|
2022-09-08 16:14:00 -05:00
|
|
|
(dominant, previous, silence) => {
|
|
|
|
|
APP.store.dispatch(dominantSpeakerChanged(dominant, previous, Boolean(silence), room));
|
|
|
|
|
});
|
2016-01-06 16:39:13 -06:00
|
|
|
|
2020-01-13 19:12:25 +02:00
|
|
|
room.on(
|
|
|
|
|
JitsiConferenceEvents.CONFERENCE_CREATED_TIMESTAMP,
|
2024-05-11 00:49:47 +10:00
|
|
|
conferenceTimestamp => {
|
|
|
|
|
APP.store.dispatch(conferenceTimestampChanged(conferenceTimestamp));
|
|
|
|
|
APP.API.notifyConferenceCreatedTimestamp(conferenceTimestamp);
|
|
|
|
|
}
|
|
|
|
|
);
|
2020-01-13 19:12:25 +02:00
|
|
|
|
2017-10-12 18:02:29 -05:00
|
|
|
room.on(
|
|
|
|
|
JitsiConferenceEvents.DISPLAY_NAME_CHANGED,
|
|
|
|
|
(id, displayName) => {
|
|
|
|
|
const formattedDisplayName
|
2019-01-15 12:28:07 +01:00
|
|
|
= getNormalizedDisplayName(displayName);
|
2021-11-26 17:39:34 +02:00
|
|
|
const state = APP.store.getState();
|
|
|
|
|
const {
|
|
|
|
|
defaultRemoteDisplayName
|
|
|
|
|
} = state['features/base/config'];
|
2017-10-12 18:02:29 -05:00
|
|
|
|
|
|
|
|
APP.store.dispatch(participantUpdated({
|
Associate remote participant w/ JitsiConference (_UPDATED)
The commit message of "Associate remote participant w/ JitsiConference
(_JOINED)" explains the motivation for this commit.
Practically, _JOINED and _LEFT combined with "Remove remote participants
who are no longer of interest" should alleviate the problem with
multiplying remote participants to an acceptable level of annoyance.
Technically though, a remote participant cannot be identified by an ID
only. The ID is (somewhat) "unique" in the context of a single
JitsiConference instance. So in order to not have to scratch our heads
over an obscure corner, racing case, it's better to always identify
remote participants by the pair id-conference. Unfortunately, that's a
bit of a high order given the existing source code. So I've implemented
the cases which are the easiest so that new source code written with
participantUpdated is more likely to identify a remote participant with
the pair id-conference.
Additionally, the commit "Reduce direct read access to the
features/base/participants redux state" brings more control back to the
functions of the feature base/participants so that one day we can (if we
choose to) do something like, for example:
If getParticipants is called with a conference, it returns the
participants from features/base/participants who are associated with the
specified conference. If no conference is specified in the function
call, then default to the conference which is the primary focus of the
app at the time of the function call. Added to the above, this should
allow us to further reduce the cases in which we're identifying remote
participants by id only and get us even closer to a more "predictable"
behavior in corner, racing cases.
2018-05-22 16:47:43 -05:00
|
|
|
conference: room,
|
2017-10-12 18:02:29 -05:00
|
|
|
id,
|
|
|
|
|
name: formattedDisplayName
|
|
|
|
|
}));
|
2022-04-14 13:07:17 -04:00
|
|
|
|
2022-04-29 10:32:16 -04:00
|
|
|
const virtualScreenshareParticipantId = getVirtualScreenshareParticipantByOwnerId(state, id)?.id;
|
2022-04-14 13:07:17 -04:00
|
|
|
|
2022-04-29 10:32:16 -04:00
|
|
|
if (virtualScreenshareParticipantId) {
|
|
|
|
|
APP.store.dispatch(
|
|
|
|
|
screenshareParticipantDisplayNameChanged(virtualScreenshareParticipantId, formattedDisplayName)
|
|
|
|
|
);
|
2022-04-14 13:07:17 -04:00
|
|
|
}
|
|
|
|
|
|
2017-12-04 21:27:17 -06:00
|
|
|
APP.API.notifyDisplayNameChanged(id, {
|
|
|
|
|
displayName: formattedDisplayName,
|
|
|
|
|
formattedDisplayName:
|
|
|
|
|
appendSuffix(
|
|
|
|
|
formattedDisplayName
|
2021-11-26 17:39:34 +02:00
|
|
|
|| defaultRemoteDisplayName)
|
2017-12-04 21:27:17 -06:00
|
|
|
});
|
2017-10-12 18:02:29 -05:00
|
|
|
}
|
|
|
|
|
);
|
2024-07-02 09:22:10 -04:00
|
|
|
|
|
|
|
|
room.on(
|
|
|
|
|
JitsiConferenceEvents.SILENT_STATUS_CHANGED,
|
|
|
|
|
(id, isSilent) => {
|
|
|
|
|
APP.store.dispatch(participantUpdated({
|
|
|
|
|
conference: room,
|
|
|
|
|
id,
|
|
|
|
|
isSilent
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
2018-06-26 17:56:22 -05:00
|
|
|
room.on(
|
|
|
|
|
JitsiConferenceEvents.BOT_TYPE_CHANGED,
|
|
|
|
|
(id, botType) => {
|
|
|
|
|
|
|
|
|
|
APP.store.dispatch(participantUpdated({
|
|
|
|
|
conference: room,
|
|
|
|
|
id,
|
|
|
|
|
botType
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
);
|
2016-01-06 16:39:13 -06:00
|
|
|
|
2018-07-17 19:31:12 +02:00
|
|
|
room.on(
|
2024-10-02 18:59:04 -05:00
|
|
|
JitsiConferenceEvents.TRANSCRIPTION_STATUS_CHANGED,
|
|
|
|
|
(status, id, abruptly) => {
|
|
|
|
|
if (status === JitsiMeetJS.constants.transcriptionStatus.ON) {
|
|
|
|
|
APP.store.dispatch(transcriberJoined(id));
|
|
|
|
|
} else if (status === JitsiMeetJS.constants.transcriptionStatus.OFF) {
|
|
|
|
|
APP.store.dispatch(transcriberLeft(id, abruptly));
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
room.on(
|
2018-07-17 19:31:12 +02:00
|
|
|
JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
|
2024-02-05 10:30:21 +01:00
|
|
|
(participant, data) => {
|
|
|
|
|
APP.store.dispatch(endpointMessageReceived(participant, data));
|
|
|
|
|
if (data?.name === ENDPOINT_TEXT_MESSAGE_NAME) {
|
|
|
|
|
APP.API.notifyEndpointTextMessageReceived({
|
|
|
|
|
senderInfo: {
|
|
|
|
|
jid: participant.getJid(),
|
|
|
|
|
id: participant.getId()
|
|
|
|
|
},
|
|
|
|
|
eventData: data
|
|
|
|
|
});
|
2020-03-20 13:51:26 +02:00
|
|
|
}
|
|
|
|
|
});
|
2018-07-17 19:31:12 +02:00
|
|
|
|
2021-09-14 17:31:30 +02:00
|
|
|
room.on(
|
|
|
|
|
JitsiConferenceEvents.NON_PARTICIPANT_MESSAGE_RECEIVED,
|
2024-02-05 10:30:21 +01:00
|
|
|
(id, data) => {
|
|
|
|
|
APP.store.dispatch(nonParticipantMessageReceived(id, data));
|
|
|
|
|
APP.API.notifyNonParticipantMessageReceived(id, data);
|
2023-06-12 18:10:42 +10:00
|
|
|
});
|
2021-09-14 17:31:30 +02:00
|
|
|
|
2017-04-18 16:49:52 -05:00
|
|
|
room.on(
|
2017-10-10 18:31:40 -05:00
|
|
|
JitsiConferenceEvents.LOCK_STATE_CHANGED,
|
2017-04-18 16:49:52 -05:00
|
|
|
(...args) => APP.store.dispatch(lockStateChanged(room, ...args)));
|
2017-04-12 17:23:43 -07:00
|
|
|
|
2024-10-28 12:24:42 -05:00
|
|
|
room.on(
|
|
|
|
|
JitsiConferenceEvents.PROPERTIES_CHANGED,
|
|
|
|
|
properties => APP.store.dispatch(conferencePropertiesChanged(properties)));
|
|
|
|
|
|
2021-06-11 11:58:45 +03:00
|
|
|
room.on(JitsiConferenceEvents.KICKED, (participant, reason, isReplaced) => {
|
|
|
|
|
if (isReplaced) {
|
|
|
|
|
// this event triggers when the local participant is kicked, `participant`
|
|
|
|
|
// is the kicker. In replace participant case, kicker is undefined,
|
|
|
|
|
// as the server initiated it. We mark in store the local participant
|
|
|
|
|
// as being replaced based on jwt.
|
|
|
|
|
const localParticipant = getLocalParticipant(APP.store.getState());
|
|
|
|
|
|
|
|
|
|
APP.store.dispatch(participantUpdated({
|
|
|
|
|
conference: room,
|
|
|
|
|
id: localParticipant.id,
|
|
|
|
|
isReplaced
|
|
|
|
|
}));
|
2021-06-24 14:33:58 +03:00
|
|
|
|
|
|
|
|
// we send readyToClose when kicked participant is replace so that
|
|
|
|
|
// embedding app can choose to dispose the iframe API on the handler.
|
|
|
|
|
APP.API.notifyReadyToClose();
|
2021-06-11 11:58:45 +03:00
|
|
|
}
|
2019-06-26 14:53:48 -07:00
|
|
|
APP.store.dispatch(kickedOut(room, participant));
|
2016-01-06 16:39:13 -06:00
|
|
|
});
|
|
|
|
|
|
2019-06-17 16:00:09 +02:00
|
|
|
room.on(JitsiConferenceEvents.PARTICIPANT_KICKED, (kicker, kicked) => {
|
|
|
|
|
APP.store.dispatch(participantKicked(kicker, kicked));
|
|
|
|
|
});
|
|
|
|
|
|
2023-01-24 13:58:58 -05:00
|
|
|
room.on(JitsiConferenceEvents.PARTICIPANT_SOURCE_UPDATED,
|
|
|
|
|
jitsiParticipant => {
|
|
|
|
|
APP.store.dispatch(participantSourcesUpdated(jitsiParticipant));
|
|
|
|
|
});
|
|
|
|
|
|
2017-10-10 18:31:40 -05:00
|
|
|
room.on(JitsiConferenceEvents.SUSPEND_DETECTED, () => {
|
2017-01-31 14:58:48 -06:00
|
|
|
APP.store.dispatch(suspendDetected());
|
2016-11-09 16:34:01 -06:00
|
|
|
});
|
|
|
|
|
|
2021-11-30 15:08:25 -05:00
|
|
|
room.on(
|
|
|
|
|
JitsiConferenceEvents.AUDIO_UNMUTE_PERMISSIONS_CHANGED,
|
|
|
|
|
disableAudioMuteChange => {
|
2021-12-07 16:48:12 -05:00
|
|
|
APP.store.dispatch(setAudioUnmutePermissions(disableAudioMuteChange));
|
2021-11-30 15:08:25 -05:00
|
|
|
});
|
|
|
|
|
room.on(
|
|
|
|
|
JitsiConferenceEvents.VIDEO_UNMUTE_PERMISSIONS_CHANGED,
|
|
|
|
|
disableVideoMuteChange => {
|
2021-12-07 16:48:12 -05:00
|
|
|
APP.store.dispatch(setVideoUnmutePermissions(disableVideoMuteChange));
|
2021-11-30 15:08:25 -05:00
|
|
|
});
|
|
|
|
|
|
2022-04-01 13:50:52 +01:00
|
|
|
room.on(
|
|
|
|
|
JitsiE2ePingEvents.E2E_RTT_CHANGED,
|
|
|
|
|
(...args) => APP.store.dispatch(e2eRttChanged(...args)));
|
|
|
|
|
|
2017-10-12 18:02:29 -05:00
|
|
|
room.addCommandListener(this.commands.defaults.ETHERPAD,
|
|
|
|
|
({ value }) => {
|
|
|
|
|
APP.UI.initEtherpad(value);
|
|
|
|
|
}
|
|
|
|
|
);
|
2016-01-06 16:39:13 -06:00
|
|
|
|
2016-06-13 16:11:44 -05:00
|
|
|
room.addCommandListener(this.commands.defaults.EMAIL, (data, from) => {
|
2017-03-23 13:01:33 -05:00
|
|
|
APP.store.dispatch(participantUpdated({
|
Associate remote participant w/ JitsiConference (_UPDATED)
The commit message of "Associate remote participant w/ JitsiConference
(_JOINED)" explains the motivation for this commit.
Practically, _JOINED and _LEFT combined with "Remove remote participants
who are no longer of interest" should alleviate the problem with
multiplying remote participants to an acceptable level of annoyance.
Technically though, a remote participant cannot be identified by an ID
only. The ID is (somewhat) "unique" in the context of a single
JitsiConference instance. So in order to not have to scratch our heads
over an obscure corner, racing case, it's better to always identify
remote participants by the pair id-conference. Unfortunately, that's a
bit of a high order given the existing source code. So I've implemented
the cases which are the easiest so that new source code written with
participantUpdated is more likely to identify a remote participant with
the pair id-conference.
Additionally, the commit "Reduce direct read access to the
features/base/participants redux state" brings more control back to the
functions of the feature base/participants so that one day we can (if we
choose to) do something like, for example:
If getParticipants is called with a conference, it returns the
participants from features/base/participants who are associated with the
specified conference. If no conference is specified in the function
call, then default to the conference which is the primary focus of the
app at the time of the function call. Added to the above, this should
allow us to further reduce the cases in which we're identifying remote
participants by id only and get us even closer to a more "predictable"
behavior in corner, racing cases.
2018-05-22 16:47:43 -05:00
|
|
|
conference: room,
|
2017-03-23 13:01:33 -05:00
|
|
|
id: from,
|
|
|
|
|
email: data.value
|
|
|
|
|
}));
|
2016-01-06 16:39:13 -06:00
|
|
|
});
|
|
|
|
|
|
2016-10-27 15:49:29 -05:00
|
|
|
room.addCommandListener(
|
|
|
|
|
this.commands.defaults.AVATAR_URL,
|
|
|
|
|
(data, from) => {
|
2024-08-26 08:47:31 -05:00
|
|
|
const participant = getParticipantByIdOrUndefined(APP.store, from);
|
|
|
|
|
|
|
|
|
|
// if already set from presence(jwt), skip the command processing
|
|
|
|
|
if (!participant?.avatarURL) {
|
|
|
|
|
APP.store.dispatch(
|
|
|
|
|
participantUpdated({
|
|
|
|
|
conference: room,
|
|
|
|
|
id: from,
|
|
|
|
|
avatarURL: data.value
|
|
|
|
|
}));
|
|
|
|
|
}
|
2017-10-02 18:08:07 -05:00
|
|
|
});
|
2016-01-06 16:39:13 -06:00
|
|
|
|
2015-12-31 17:23:23 +02:00
|
|
|
room.on(
|
2017-10-10 18:31:40 -05:00
|
|
|
JitsiConferenceEvents.START_MUTED_POLICY_CHANGED,
|
2016-03-02 17:39:39 +02:00
|
|
|
({ audio, video }) => {
|
2025-07-21 16:55:22 +03:00
|
|
|
APP.store.dispatch(onStartMutedPolicyChanged(audio, video));
|
|
|
|
|
|
|
|
|
|
const state = APP.store.getState();
|
|
|
|
|
|
|
|
|
|
updateTrackMuteState(state, APP.store.dispatch, true);
|
|
|
|
|
updateTrackMuteState(state, APP.store.dispatch, false);
|
2016-01-06 16:39:13 -06:00
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
2017-08-09 12:40:03 -07:00
|
|
|
room.on(
|
2017-10-10 18:31:40 -05:00
|
|
|
JitsiConferenceEvents.DATA_CHANNEL_OPENED, () => {
|
2017-08-09 12:40:03 -07:00
|
|
|
APP.store.dispatch(dataChannelOpened());
|
2022-11-17 12:10:29 +01:00
|
|
|
APP.store.dispatch(hideNotification(DATA_CHANNEL_CLOSED_NOTIFICATION_ID));
|
2017-08-09 12:40:03 -07:00
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
2022-11-15 16:54:24 +01:00
|
|
|
room.on(
|
|
|
|
|
JitsiConferenceEvents.DATA_CHANNEL_CLOSED, ev => {
|
2024-04-29 14:05:59 -04:00
|
|
|
const state = APP.store.getState();
|
|
|
|
|
const { dataChannelOpen } = state['features/base/conference'];
|
|
|
|
|
const timeout = typeof dataChannelOpen === 'undefined' ? 15000 : 60000;
|
|
|
|
|
|
|
|
|
|
// Show the notification only when the data channel connection doesn't get re-established in 60 secs if
|
|
|
|
|
// it was already established at the beginning of the call, show it sooner otherwise. This notification
|
|
|
|
|
// can be confusing and alarming to users even when there is no significant impact to user experience
|
|
|
|
|
// if the the reconnect happens immediately.
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
const { dataChannelOpen: open } = APP.store.getState()['features/base/conference'];
|
|
|
|
|
|
|
|
|
|
if (!open) {
|
|
|
|
|
const descriptionKey = getSsrcRewritingFeatureFlag(state)
|
|
|
|
|
? 'notify.dataChannelClosedDescriptionWithAudio' : 'notify.dataChannelClosedDescription';
|
|
|
|
|
const titleKey = getSsrcRewritingFeatureFlag(state)
|
|
|
|
|
? 'notify.dataChannelClosedWithAudio' : 'notify.dataChannelClosed';
|
|
|
|
|
|
|
|
|
|
APP.store.dispatch(dataChannelClosed(ev.code, ev.reason));
|
|
|
|
|
APP.store.dispatch(showWarningNotification({
|
|
|
|
|
descriptionKey,
|
|
|
|
|
titleKey,
|
|
|
|
|
uid: DATA_CHANNEL_CLOSED_NOTIFICATION_ID
|
|
|
|
|
}, NOTIFICATION_TIMEOUT_TYPE.STICKY));
|
|
|
|
|
}
|
|
|
|
|
}, timeout);
|
2022-11-15 16:54:24 +01:00
|
|
|
}
|
|
|
|
|
);
|
2025-03-26 13:18:30 -05:00
|
|
|
|
|
|
|
|
room.on(JitsiConferenceEvents.PERMISSIONS_RECEIVED, p => {
|
|
|
|
|
const localParticipant = getLocalParticipant(APP.store.getState());
|
|
|
|
|
|
|
|
|
|
APP.store.dispatch(participantUpdated({
|
|
|
|
|
id: localParticipant.id,
|
|
|
|
|
local: true,
|
|
|
|
|
features: p
|
|
|
|
|
}));
|
|
|
|
|
});
|
2024-04-16 17:10:36 -05:00
|
|
|
},
|
2022-11-15 16:54:24 +01:00
|
|
|
|
2024-04-16 17:10:36 -05:00
|
|
|
/**
|
|
|
|
|
* Handles audio device changes.
|
|
|
|
|
*
|
|
|
|
|
* @param {string} cameraDeviceId - The new device id.
|
|
|
|
|
* @returns {Promise}
|
|
|
|
|
*/
|
|
|
|
|
async onAudioDeviceChanged(micDeviceId) {
|
|
|
|
|
const audioWasMuted = this.isLocalAudioMuted();
|
|
|
|
|
|
|
|
|
|
// Disable noise suppression if it was enabled on the previous track.
|
|
|
|
|
await APP.store.dispatch(setNoiseSuppressionEnabled(false));
|
|
|
|
|
|
|
|
|
|
// When the 'default' mic needs to be selected, we need to pass the real device id to gUM instead of
|
|
|
|
|
// 'default' in order to get the correct MediaStreamTrack from chrome because of the following bug.
|
|
|
|
|
// https://bugs.chromium.org/p/chromium/issues/detail?id=997689.
|
|
|
|
|
const isDefaultMicSelected = micDeviceId === 'default';
|
|
|
|
|
const selectedDeviceId = isDefaultMicSelected
|
|
|
|
|
? getDefaultDeviceId(APP.store.getState(), 'audioInput')
|
|
|
|
|
: micDeviceId;
|
|
|
|
|
|
|
|
|
|
logger.info(`Switching audio input device to ${selectedDeviceId}`);
|
|
|
|
|
sendAnalytics(createDeviceChangedEvent('audio', 'input'));
|
|
|
|
|
createLocalTracksF({
|
|
|
|
|
devices: [ 'audio' ],
|
|
|
|
|
micDeviceId: selectedDeviceId
|
|
|
|
|
})
|
|
|
|
|
.then(([ stream ]) => {
|
|
|
|
|
// if audio was muted before changing the device, mute
|
|
|
|
|
// with the new device
|
|
|
|
|
if (audioWasMuted) {
|
|
|
|
|
return stream.mute()
|
|
|
|
|
.then(() => stream);
|
|
|
|
|
}
|
2023-06-01 17:49:20 -04:00
|
|
|
|
2024-04-16 17:10:36 -05:00
|
|
|
return stream;
|
|
|
|
|
})
|
|
|
|
|
.then(async stream => {
|
|
|
|
|
await this._maybeApplyAudioMixerEffect(stream);
|
2017-10-17 13:20:33 -05:00
|
|
|
|
2024-04-16 17:10:36 -05:00
|
|
|
return this.useAudioStream(stream);
|
|
|
|
|
})
|
|
|
|
|
.then(() => {
|
2025-09-05 22:52:35 +02:00
|
|
|
const state = APP.store.getState();
|
|
|
|
|
const localAudio = getLocalJitsiAudioTrack(state);
|
|
|
|
|
const settings = getLocalJitsiAudioTrackSettings(state);
|
|
|
|
|
|
|
|
|
|
APP.store.dispatch(setAudioSettings(settings));
|
2017-10-17 13:47:39 -05:00
|
|
|
|
2024-04-16 17:10:36 -05:00
|
|
|
if (localAudio && isDefaultMicSelected) {
|
|
|
|
|
// workaround for the default device to be shown as selected in the
|
|
|
|
|
// settings even when the real device id was passed to gUM because of the
|
|
|
|
|
// above mentioned chrome bug.
|
|
|
|
|
localAudio._realDeviceId = localAudio.deviceId = 'default';
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.catch(err => {
|
|
|
|
|
logger.error(`Failed to switch to selected audio input device ${selectedDeviceId}, error=${err}`);
|
|
|
|
|
APP.store.dispatch(notifyMicError(err));
|
|
|
|
|
});
|
|
|
|
|
},
|
2019-11-26 05:57:03 -05:00
|
|
|
|
2024-04-16 17:10:36 -05:00
|
|
|
/**
|
|
|
|
|
* Handles video device changes.
|
|
|
|
|
*
|
|
|
|
|
* @param {string} cameraDeviceId - The new device id.
|
|
|
|
|
* @returns {void}
|
|
|
|
|
*/
|
|
|
|
|
onVideoDeviceChanged(cameraDeviceId) {
|
|
|
|
|
const videoWasMuted = this.isLocalVideoMuted();
|
|
|
|
|
const localVideoTrack = getLocalJitsiVideoTrack(APP.store.getState());
|
2019-12-04 15:28:42 -05:00
|
|
|
|
2024-04-16 17:10:36 -05:00
|
|
|
if (localVideoTrack?.getDeviceId() === cameraDeviceId) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2020-01-13 08:21:31 -06:00
|
|
|
|
2024-04-16 17:10:36 -05:00
|
|
|
sendAnalytics(createDeviceChangedEvent('video', 'input'));
|
|
|
|
|
|
|
|
|
|
createLocalTracksF({
|
|
|
|
|
devices: [ 'video' ],
|
|
|
|
|
cameraDeviceId
|
|
|
|
|
})
|
|
|
|
|
.then(([ stream ]) => {
|
|
|
|
|
// if we are in audio only mode or video was muted before
|
|
|
|
|
// changing device, then mute
|
|
|
|
|
if (this.isAudioOnly() || videoWasMuted) {
|
|
|
|
|
return stream.mute()
|
|
|
|
|
.then(() => stream);
|
2016-02-09 12:19:43 +02:00
|
|
|
}
|
|
|
|
|
|
2024-04-16 17:10:36 -05:00
|
|
|
return stream;
|
|
|
|
|
})
|
|
|
|
|
.then(stream => {
|
|
|
|
|
logger.info(`Switching the local video device to ${cameraDeviceId}.`);
|
2017-10-17 13:20:33 -05:00
|
|
|
|
2024-04-16 17:10:36 -05:00
|
|
|
return this.useVideoStream(stream);
|
|
|
|
|
})
|
|
|
|
|
.catch(error => {
|
|
|
|
|
logger.error(`Failed to switch to selected camera:${cameraDeviceId}, error:${error}`);
|
2020-03-26 14:17:44 +02:00
|
|
|
|
2024-04-16 17:10:36 -05:00
|
|
|
return APP.store.dispatch(notifyCameraError(error));
|
|
|
|
|
});
|
|
|
|
|
},
|
2016-02-09 12:19:43 +02:00
|
|
|
|
2024-04-16 17:10:36 -05:00
|
|
|
/**
|
|
|
|
|
* Handles audio only changes.
|
|
|
|
|
*/
|
|
|
|
|
onToggleAudioOnly() {
|
|
|
|
|
// Immediately update the UI by having remote videos and the large video update themselves.
|
|
|
|
|
const displayedUserId = APP.UI.getLargeVideoID();
|
2021-01-21 14:46:47 -06:00
|
|
|
|
2024-04-16 17:10:36 -05:00
|
|
|
if (displayedUserId) {
|
|
|
|
|
APP.UI.updateLargeVideo(displayedUserId, true);
|
|
|
|
|
}
|
2016-04-25 15:39:31 -05:00
|
|
|
},
|
2017-10-12 18:02:29 -05:00
|
|
|
|
2019-07-12 14:08:34 +01:00
|
|
|
/**
|
|
|
|
|
* Cleanups local conference on suspend.
|
|
|
|
|
*/
|
|
|
|
|
onSuspendDetected() {
|
|
|
|
|
// After wake up, we will be in a state where conference is left
|
|
|
|
|
// there will be dialog shown to user.
|
|
|
|
|
// We do not want video/audio as we show an overlay and after it
|
|
|
|
|
// user need to rejoin or close, while waking up we can detect
|
|
|
|
|
// camera wakeup as a problem with device.
|
|
|
|
|
// We also do not care about device change, which happens
|
|
|
|
|
// on resume after suspending PC.
|
|
|
|
|
if (this.deviceChangeListener) {
|
|
|
|
|
JitsiMeetJS.mediaDevices.removeEventListener(
|
|
|
|
|
JitsiMediaDevicesEvents.DEVICE_LIST_CHANGED,
|
|
|
|
|
this.deviceChangeListener);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2017-11-16 14:54:49 -08:00
|
|
|
/**
|
|
|
|
|
* Callback invoked when the conference has been successfully joined.
|
|
|
|
|
* Initializes the UI and various other features.
|
|
|
|
|
*
|
|
|
|
|
* @private
|
|
|
|
|
* @returns {void}
|
|
|
|
|
*/
|
|
|
|
|
_onConferenceJoined() {
|
2022-02-17 16:25:31 -06:00
|
|
|
const { dispatch } = APP.store;
|
|
|
|
|
|
2017-11-16 14:54:49 -08:00
|
|
|
APP.UI.initConference();
|
|
|
|
|
|
2022-02-17 16:25:31 -06:00
|
|
|
dispatch(conferenceJoined(room));
|
|
|
|
|
|
|
|
|
|
const jwt = APP.store.getState()['features/base/jwt'];
|
|
|
|
|
|
|
|
|
|
if (jwt?.user?.hiddenFromRecorder) {
|
|
|
|
|
dispatch(muteLocal(true, MEDIA_TYPE.AUDIO));
|
|
|
|
|
dispatch(muteLocal(true, MEDIA_TYPE.VIDEO));
|
|
|
|
|
dispatch(setAudioUnmutePermissions(true, true));
|
|
|
|
|
dispatch(setVideoUnmutePermissions(true, true));
|
|
|
|
|
}
|
2017-11-16 14:54:49 -08:00
|
|
|
},
|
|
|
|
|
|
2016-06-13 14:49:00 +03:00
|
|
|
/**
|
2019-05-07 09:53:01 +01:00
|
|
|
* Updates the list of current devices.
|
|
|
|
|
* @param {boolean} setDeviceListChangeHandler - Whether to add the deviceList change handlers.
|
2016-06-13 14:49:00 +03:00
|
|
|
* @private
|
2018-08-06 08:24:59 -07:00
|
|
|
* @returns {Promise}
|
2016-06-13 14:49:00 +03:00
|
|
|
*/
|
2019-05-07 09:53:01 +01:00
|
|
|
_initDeviceList(setDeviceListChangeHandler = false) {
|
2018-07-11 22:57:44 -05:00
|
|
|
const { mediaDevices } = JitsiMeetJS;
|
|
|
|
|
|
2025-04-14 15:49:27 +02:00
|
|
|
if (mediaDevices.isDeviceChangeAvailable()) {
|
2019-05-07 09:53:01 +01:00
|
|
|
if (setDeviceListChangeHandler) {
|
|
|
|
|
this.deviceChangeListener = devices =>
|
|
|
|
|
window.setTimeout(() => this._onDeviceListChanged(devices), 0);
|
|
|
|
|
mediaDevices.addEventListener(
|
|
|
|
|
JitsiMediaDevicesEvents.DEVICE_LIST_CHANGED,
|
|
|
|
|
this.deviceChangeListener);
|
|
|
|
|
}
|
2018-08-06 08:24:59 -07:00
|
|
|
|
|
|
|
|
const { dispatch } = APP.store;
|
|
|
|
|
|
|
|
|
|
return dispatch(getAvailableDevices())
|
2024-10-25 10:28:31 -05:00
|
|
|
.then(() => {
|
|
|
|
|
this.updateAudioIconEnabled();
|
|
|
|
|
this.updateVideoIconEnabled();
|
2018-08-06 08:24:59 -07:00
|
|
|
});
|
core: refactor routing
Unfortunately, as the Jitsi Meet development evolved the routing mechanism
became more complex and thre logic ended up spread across multiple parts of the
codebase, which made it hard to follow and extend.
This change aims to fix that by rewriting the routing logic and centralizing it
in (pretty much) a single place, with no implicit inter-dependencies.
In order to arrive there, however, some extra changes were needed, which were
not caught early enough and are thus part of this change:
- JitsiMeetJS initialization is now synchronous: there is nothing async about
it, and the only async requirement (Temasys support) was lifted. See [0].
- WebRTC support can be detected early: building on top of the above, WebRTC
support can now be detected immediately, so take advantage of this to simplify
how we handle unsupported browsers. See [0].
The new router takes decissions based on the Redux state at the time of
invocation. A route can be represented by either a component or a URl reference,
with the latter taking precedence. On mobile, obviously, there is no concept of
URL reference so routing is based solely on components.
[0]: https://github.com/jitsi/lib-jitsi-meet/pull/779
2018-06-29 09:58:31 +02:00
|
|
|
}
|
2018-08-06 08:24:59 -07:00
|
|
|
|
|
|
|
|
return Promise.resolve();
|
2016-06-13 14:49:00 +03:00
|
|
|
},
|
2017-10-12 18:02:29 -05:00
|
|
|
|
2016-06-13 14:49:00 +03:00
|
|
|
/**
|
|
|
|
|
* Event listener for JitsiMediaDevicesEvents.DEVICE_LIST_CHANGED to
|
|
|
|
|
* handle change of available media devices.
|
|
|
|
|
* @private
|
|
|
|
|
* @param {MediaDeviceInfo[]} devices
|
|
|
|
|
* @returns {Promise}
|
|
|
|
|
*/
|
2022-12-15 14:00:21 -05:00
|
|
|
async _onDeviceListChanged(devices) {
|
2023-09-21 20:06:55 -05:00
|
|
|
const state = APP.store.getState();
|
|
|
|
|
const { filteredDevices, ignoredDevices } = filterIgnoredDevices(devices);
|
|
|
|
|
const oldDevices = state['features/base/devices'].availableDevices;
|
|
|
|
|
|
|
|
|
|
if (!areDevicesDifferent(flattenAvailableDevices(oldDevices), filteredDevices)) {
|
|
|
|
|
return Promise.resolve();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logDevices(ignoredDevices, 'Ignored devices on device list changed:');
|
|
|
|
|
|
|
|
|
|
const localAudio = getLocalJitsiAudioTrack(state);
|
|
|
|
|
const localVideo = getLocalJitsiVideoTrack(state);
|
2019-05-03 18:25:33 +01:00
|
|
|
|
2023-09-21 20:06:55 -05:00
|
|
|
APP.store.dispatch(updateDeviceList(filteredDevices));
|
2016-06-13 14:49:00 +03:00
|
|
|
|
2021-10-25 11:53:45 -07:00
|
|
|
// Firefox users can choose their preferred device in the gUM prompt. In that case
|
|
|
|
|
// we should respect that and not attempt to switch to the preferred device from
|
|
|
|
|
// our settings.
|
2023-09-21 20:06:55 -05:00
|
|
|
const newLabelsOnly = mediaDeviceHelper.newDeviceListAddedLabelsOnly(oldDevices, filteredDevices);
|
2017-10-12 18:02:29 -05:00
|
|
|
const newDevices
|
|
|
|
|
= mediaDeviceHelper.getNewMediaDevicesAfterDeviceListChanged(
|
2023-09-21 20:06:55 -05:00
|
|
|
filteredDevices,
|
2021-09-13 12:33:04 -05:00
|
|
|
localVideo,
|
2021-10-25 11:53:45 -07:00
|
|
|
localAudio,
|
|
|
|
|
newLabelsOnly);
|
2017-10-12 18:02:29 -05:00
|
|
|
const promises = [];
|
2019-04-12 17:10:38 +01:00
|
|
|
const requestedInput = {
|
|
|
|
|
audio: Boolean(newDevices.audioinput),
|
|
|
|
|
video: Boolean(newDevices.videoinput)
|
|
|
|
|
};
|
2016-06-13 14:49:00 +03:00
|
|
|
|
|
|
|
|
if (typeof newDevices.audiooutput !== 'undefined') {
|
2018-07-13 09:21:26 -07:00
|
|
|
const { dispatch } = APP.store;
|
|
|
|
|
const setAudioOutputPromise
|
|
|
|
|
= setAudioOutputDeviceId(newDevices.audiooutput, dispatch)
|
2023-06-01 17:49:20 -04:00
|
|
|
.catch(err => {
|
|
|
|
|
logger.error(`Failed to set the audio output device to ${newDevices.audiooutput} - ${err}`);
|
|
|
|
|
});
|
2018-07-13 09:21:26 -07:00
|
|
|
|
|
|
|
|
promises.push(setAudioOutputPromise);
|
2016-06-13 14:49:00 +03:00
|
|
|
}
|
|
|
|
|
|
2019-04-12 17:10:38 +01:00
|
|
|
// Handles the use case when the default device is changed (we are always stopping the streams because it's
|
|
|
|
|
// simpler):
|
|
|
|
|
// If the default device is changed we need to first stop the local streams and then call GUM. Otherwise GUM
|
|
|
|
|
// will return a stream using the old default device.
|
2021-09-13 12:33:04 -05:00
|
|
|
if (requestedInput.audio && localAudio) {
|
|
|
|
|
localAudio.stopStream();
|
2019-04-12 17:10:38 +01:00
|
|
|
}
|
|
|
|
|
|
2021-09-13 12:33:04 -05:00
|
|
|
if (requestedInput.video && localVideo) {
|
|
|
|
|
localVideo.stopStream();
|
2019-04-12 17:10:38 +01:00
|
|
|
}
|
|
|
|
|
|
2019-05-03 18:25:33 +01:00
|
|
|
// Let's handle unknown/non-preferred devices
|
2022-12-15 14:00:21 -05:00
|
|
|
const newAvailDevices = APP.store.getState()['features/base/devices'].availableDevices;
|
2019-05-20 21:35:42 +01:00
|
|
|
let newAudioDevices = [];
|
|
|
|
|
let oldAudioDevices = [];
|
2019-05-03 18:25:33 +01:00
|
|
|
|
|
|
|
|
if (typeof newDevices.audiooutput === 'undefined') {
|
2019-05-20 21:35:42 +01:00
|
|
|
newAudioDevices = newAvailDevices.audioOutput;
|
|
|
|
|
oldAudioDevices = oldDevices.audioOutput;
|
2019-05-03 18:25:33 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!requestedInput.audio) {
|
2019-05-20 21:35:42 +01:00
|
|
|
newAudioDevices = newAudioDevices.concat(newAvailDevices.audioInput);
|
|
|
|
|
oldAudioDevices = oldAudioDevices.concat(oldDevices.audioInput);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// check for audio
|
|
|
|
|
if (newAudioDevices.length > 0) {
|
2022-12-15 14:00:21 -05:00
|
|
|
APP.store.dispatch(checkAndNotifyForNewDevice(newAudioDevices, oldAudioDevices));
|
2019-05-03 18:25:33 +01:00
|
|
|
}
|
|
|
|
|
|
2019-05-20 21:35:42 +01:00
|
|
|
// check for video
|
2023-06-01 17:49:20 -04:00
|
|
|
if (requestedInput.video) {
|
2022-12-15 14:00:21 -05:00
|
|
|
APP.store.dispatch(checkAndNotifyForNewDevice(newAvailDevices.videoInput, oldDevices.videoInput));
|
2019-05-03 18:25:33 +01:00
|
|
|
}
|
|
|
|
|
|
2022-12-15 14:00:21 -05:00
|
|
|
// When the 'default' mic needs to be selected, we need to pass the real device id to gUM instead of 'default'
|
|
|
|
|
// in order to get the correct MediaStreamTrack from chrome because of the following bug.
|
2020-06-03 17:49:08 -04:00
|
|
|
// https://bugs.chromium.org/p/chromium/issues/detail?id=997689
|
|
|
|
|
const hasDefaultMicChanged = newDevices.audioinput === 'default';
|
|
|
|
|
|
2022-12-15 14:00:21 -05:00
|
|
|
// When the local video is muted and a preferred device is connected, update the settings and remove the track
|
|
|
|
|
// from the conference. A new track will be created and replaced when the user unmutes their camera.
|
2020-10-22 14:48:17 -05:00
|
|
|
if (requestedInput.video && this.isLocalVideoMuted()) {
|
2022-12-15 14:00:21 -05:00
|
|
|
APP.store.dispatch(updateSettings({
|
2020-10-22 14:48:17 -05:00
|
|
|
cameraDeviceId: newDevices.videoinput
|
|
|
|
|
}));
|
2022-12-15 14:00:21 -05:00
|
|
|
requestedInput.video = false;
|
2020-10-22 14:48:17 -05:00
|
|
|
delete newDevices.videoinput;
|
|
|
|
|
|
2022-12-15 14:00:21 -05:00
|
|
|
// Remove the track from the conference.
|
|
|
|
|
if (localVideo) {
|
|
|
|
|
await this.useVideoStream(null);
|
|
|
|
|
logger.debug('_onDeviceListChanged: Removed the current video track.');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// When the local audio is muted and a preferred device is connected, update the settings and remove the track
|
|
|
|
|
// from the conference. A new track will be created and replaced when the user unmutes their mic.
|
|
|
|
|
if (requestedInput.audio && this.isLocalAudioMuted()) {
|
|
|
|
|
APP.store.dispatch(updateSettings({
|
|
|
|
|
micDeviceId: newDevices.audioinput
|
|
|
|
|
}));
|
|
|
|
|
requestedInput.audio = false;
|
|
|
|
|
delete newDevices.audioinput;
|
2020-10-22 14:48:17 -05:00
|
|
|
|
2022-12-15 14:00:21 -05:00
|
|
|
// Remove the track from the conference.
|
|
|
|
|
if (localAudio) {
|
|
|
|
|
await this.useAudioStream(null);
|
|
|
|
|
logger.debug('_onDeviceListChanged: Removed the current audio track.');
|
|
|
|
|
}
|
2020-10-22 14:48:17 -05:00
|
|
|
}
|
|
|
|
|
|
2022-12-15 14:00:21 -05:00
|
|
|
// Create the tracks and replace them only if the user is unmuted.
|
|
|
|
|
if (requestedInput.audio || requestedInput.video) {
|
|
|
|
|
let tracks = [];
|
2023-06-01 17:49:20 -04:00
|
|
|
const realAudioDeviceId = hasDefaultMicChanged
|
|
|
|
|
? getDefaultDeviceId(APP.store.getState(), 'audioInput') : newDevices.audioinput;
|
2022-12-15 14:00:21 -05:00
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
tracks = await mediaDeviceHelper.createLocalTracksAfterDeviceListChanged(
|
2017-08-14 15:25:37 +02:00
|
|
|
createLocalTracksF,
|
2023-06-01 17:49:20 -04:00
|
|
|
requestedInput.video ? newDevices.videoinput : null,
|
|
|
|
|
requestedInput.audio ? realAudioDeviceId : null
|
|
|
|
|
);
|
2022-12-15 14:00:21 -05:00
|
|
|
} catch (error) {
|
|
|
|
|
logger.error(`Track creation failed on device change, ${error}`);
|
2018-09-04 08:29:50 -07:00
|
|
|
|
2022-12-15 14:00:21 -05:00
|
|
|
return Promise.reject(error);
|
|
|
|
|
}
|
2016-06-13 14:49:00 +03:00
|
|
|
|
2022-12-15 14:00:21 -05:00
|
|
|
for (const track of tracks) {
|
|
|
|
|
if (track.isAudioTrack()) {
|
|
|
|
|
promises.push(
|
|
|
|
|
this.useAudioStream(track)
|
|
|
|
|
.then(() => {
|
|
|
|
|
hasDefaultMicChanged && (track._realDeviceId = track.deviceId = 'default');
|
|
|
|
|
}));
|
|
|
|
|
} else {
|
|
|
|
|
promises.push(
|
2023-11-24 14:16:08 +02:00
|
|
|
this.useVideoStream(track));
|
2022-12-15 14:00:21 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2016-06-13 14:49:00 +03:00
|
|
|
|
|
|
|
|
return Promise.all(promises)
|
|
|
|
|
.then(() => {
|
2024-10-25 10:28:31 -05:00
|
|
|
this.updateAudioIconEnabled();
|
|
|
|
|
this.updateVideoIconEnabled();
|
2016-06-13 14:49:00 +03:00
|
|
|
});
|
2016-06-20 16:13:17 -05:00
|
|
|
},
|
2017-07-24 15:56:57 +02:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Determines whether or not the audio button should be enabled.
|
|
|
|
|
*/
|
|
|
|
|
updateAudioIconEnabled() {
|
2021-09-13 12:33:04 -05:00
|
|
|
const localAudio = getLocalJitsiAudioTrack(APP.store.getState());
|
|
|
|
|
const audioMediaDevices = APP.store.getState()['features/base/devices'].availableDevices.audioInput;
|
|
|
|
|
const audioDeviceCount = audioMediaDevices ? audioMediaDevices.length : 0;
|
2017-07-24 15:56:57 +02:00
|
|
|
|
|
|
|
|
// The audio functionality is considered available if there are any
|
|
|
|
|
// audio devices detected or if the local audio stream already exists.
|
2021-09-13 12:33:04 -05:00
|
|
|
const available = audioDeviceCount > 0 || Boolean(localAudio);
|
2017-07-24 15:56:57 +02:00
|
|
|
|
|
|
|
|
APP.store.dispatch(setAudioAvailable(available));
|
|
|
|
|
},
|
|
|
|
|
|
2017-06-28 09:15:46 -05:00
|
|
|
/**
|
|
|
|
|
* Determines whether or not the video button should be enabled.
|
|
|
|
|
*/
|
|
|
|
|
updateVideoIconEnabled() {
|
|
|
|
|
const videoMediaDevices
|
2019-03-28 16:29:30 +00:00
|
|
|
= APP.store.getState()['features/base/devices'].availableDevices.videoInput;
|
2017-06-28 09:15:46 -05:00
|
|
|
const videoDeviceCount
|
|
|
|
|
= videoMediaDevices ? videoMediaDevices.length : 0;
|
2021-09-13 12:33:04 -05:00
|
|
|
const localVideo = getLocalJitsiVideoTrack(APP.store.getState());
|
2017-10-12 18:02:29 -05:00
|
|
|
|
2017-06-28 09:15:46 -05:00
|
|
|
// The video functionality is considered available if there are any
|
|
|
|
|
// video devices detected or if there is local video stream already
|
|
|
|
|
// active which could be either screensharing stream or a video track
|
|
|
|
|
// created before the permissions were rejected (through browser
|
|
|
|
|
// config).
|
2021-09-13 12:33:04 -05:00
|
|
|
const available = videoDeviceCount > 0 || Boolean(localVideo);
|
2017-06-28 09:15:46 -05:00
|
|
|
|
|
|
|
|
APP.store.dispatch(setVideoAvailable(available));
|
2017-08-04 11:15:11 +03:00
|
|
|
APP.API.notifyVideoAvailabilityChanged(available);
|
2017-06-28 09:15:46 -05:00
|
|
|
},
|
2016-06-20 16:13:17 -05:00
|
|
|
|
2016-10-05 16:33:09 -05:00
|
|
|
/**
|
|
|
|
|
* Disconnect from the conference and optionally request user feedback.
|
|
|
|
|
* @param {boolean} [requestFeedback=false] if user feedback should be
|
2023-08-28 15:14:03 +03:00
|
|
|
* @param {string} [hangupReason] the reason for leaving the meeting
|
2016-10-05 16:33:09 -05:00
|
|
|
* requested
|
2025-03-05 09:52:22 +02:00
|
|
|
* @param {boolean} [notifyOnConferenceTermination] whether to notify
|
|
|
|
|
* the user on conference termination
|
2016-10-05 16:33:09 -05:00
|
|
|
*/
|
2025-03-05 09:52:22 +02:00
|
|
|
hangup(requestFeedback = false, hangupReason, notifyOnConferenceTermination) {
|
2020-11-13 22:09:25 -06:00
|
|
|
APP.store.dispatch(disableReceiver());
|
2019-01-01 13:19:34 -08:00
|
|
|
|
2019-01-26 12:53:11 -08:00
|
|
|
this._stopProxyConnection();
|
|
|
|
|
|
2019-01-01 13:19:34 -08:00
|
|
|
APP.store.dispatch(destroyLocalTracks());
|
|
|
|
|
this._localTracksInitialized = false;
|
2017-06-05 13:19:25 -05:00
|
|
|
|
2018-05-01 12:42:08 -07:00
|
|
|
// Remove unnecessary event listeners from firing callbacks.
|
2018-07-09 11:46:26 -07:00
|
|
|
if (this.deviceChangeListener) {
|
|
|
|
|
JitsiMeetJS.mediaDevices.removeEventListener(
|
|
|
|
|
JitsiMediaDevicesEvents.DEVICE_LIST_CHANGED,
|
|
|
|
|
this.deviceChangeListener);
|
|
|
|
|
}
|
2018-05-01 12:42:08 -07:00
|
|
|
|
2023-11-03 15:45:40 -05:00
|
|
|
let feedbackResultPromise = Promise.resolve({});
|
2017-08-07 09:20:44 -07:00
|
|
|
|
|
|
|
|
if (requestFeedback) {
|
2023-11-03 15:45:40 -05:00
|
|
|
const feedbackDialogClosed = (feedbackResult = {}) => {
|
2025-03-05 09:52:22 +02:00
|
|
|
if (!feedbackResult.wasDialogShown && hangupReason && notifyOnConferenceTermination) {
|
2023-11-03 15:45:40 -05:00
|
|
|
return APP.store.dispatch(
|
|
|
|
|
openLeaveReasonDialog(hangupReason)).then(() => feedbackResult);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Promise.resolve(feedbackResult);
|
|
|
|
|
};
|
2017-10-12 18:02:29 -05:00
|
|
|
|
2023-11-03 15:45:40 -05:00
|
|
|
feedbackResultPromise
|
|
|
|
|
= APP.store.dispatch(maybeOpenFeedbackDialog(room, hangupReason))
|
|
|
|
|
.then(feedbackDialogClosed, feedbackDialogClosed);
|
2017-08-07 09:20:44 -07:00
|
|
|
}
|
|
|
|
|
|
2023-11-03 15:45:40 -05:00
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
APP.store.dispatch(maybeRedirectToWelcomePage(feedback.value ?? {}));
|
|
|
|
|
});
|
2023-08-28 15:14:03 +03:00
|
|
|
|
|
|
|
|
|
2017-01-12 15:53:17 -06:00
|
|
|
},
|
|
|
|
|
|
2021-09-14 17:31:30 +02:00
|
|
|
/**
|
|
|
|
|
* Leaves the room.
|
|
|
|
|
*
|
2022-07-14 03:10:08 -04:00
|
|
|
* @param {boolean} doDisconnect - Whether leaving the room should also terminate the connection.
|
2022-08-26 12:53:32 +03:00
|
|
|
* @param {string} reason - reason for leaving the room.
|
2021-09-14 17:31:30 +02:00
|
|
|
* @returns {Promise}
|
|
|
|
|
*/
|
2023-11-03 15:45:40 -05:00
|
|
|
leaveRoom(doDisconnect = true, reason = '') {
|
2018-08-01 15:37:15 -05:00
|
|
|
APP.store.dispatch(conferenceWillLeave(room));
|
|
|
|
|
|
2023-05-16 16:24:26 -05:00
|
|
|
const maybeDisconnect = () => {
|
|
|
|
|
if (doDisconnect) {
|
|
|
|
|
return disconnect();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2020-04-29 15:52:44 +03:00
|
|
|
if (room && room.isJoined()) {
|
2023-05-16 16:24:26 -05:00
|
|
|
return room.leave(reason).then(() => maybeDisconnect())
|
|
|
|
|
.catch(e => {
|
|
|
|
|
logger.error(e);
|
|
|
|
|
|
|
|
|
|
return maybeDisconnect();
|
2021-11-30 10:13:44 +01:00
|
|
|
});
|
2019-08-13 14:59:41 +03:00
|
|
|
}
|
|
|
|
|
|
2023-05-16 16:24:26 -05:00
|
|
|
return maybeDisconnect();
|
2018-08-01 15:37:15 -05:00
|
|
|
},
|
|
|
|
|
|
2017-01-12 15:53:17 -06:00
|
|
|
/**
|
|
|
|
|
* Changes the email for the local user
|
|
|
|
|
* @param email {string} the new email
|
|
|
|
|
*/
|
|
|
|
|
changeLocalEmail(email = '') {
|
2017-10-12 18:02:29 -05:00
|
|
|
const formattedEmail = String(email).trim();
|
2017-01-12 15:53:17 -06:00
|
|
|
|
2018-04-12 21:58:20 +02:00
|
|
|
APP.store.dispatch(updateSettings({
|
|
|
|
|
email: formattedEmail
|
|
|
|
|
}));
|
2022-06-29 09:41:25 +03:00
|
|
|
|
2017-10-12 18:02:29 -05:00
|
|
|
sendData(commands.EMAIL, formattedEmail);
|
2017-01-12 15:53:17 -06:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Changes the avatar url for the local user
|
|
|
|
|
* @param url {string} the new url
|
|
|
|
|
*/
|
|
|
|
|
changeLocalAvatarUrl(url = '') {
|
2017-10-12 18:02:29 -05:00
|
|
|
const formattedUrl = String(url).trim();
|
2017-01-12 15:53:17 -06:00
|
|
|
|
2018-04-12 21:58:20 +02:00
|
|
|
APP.store.dispatch(updateSettings({
|
|
|
|
|
avatarURL: formattedUrl
|
|
|
|
|
}));
|
2022-06-29 09:41:25 +03:00
|
|
|
|
2017-01-12 15:53:17 -06:00
|
|
|
sendData(commands.AVATAR_URL, url);
|
2016-12-09 17:15:04 -06:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Sends a message via the data channel.
|
2017-01-17 18:16:18 -06:00
|
|
|
* @param {string} to the id of the endpoint that should receive the
|
|
|
|
|
* message. If "" - the message will be sent to all participants.
|
|
|
|
|
* @param {object} payload the payload of the message.
|
2016-12-09 17:15:04 -06:00
|
|
|
* @throws NetworkError or InvalidStateError or Error if the operation
|
|
|
|
|
* fails.
|
|
|
|
|
*/
|
2017-04-11 14:40:03 -05:00
|
|
|
sendEndpointMessage(to, payload) {
|
2016-12-09 17:15:04 -06:00
|
|
|
room.sendEndpointMessage(to, payload);
|
2017-01-23 12:07:08 -06:00
|
|
|
},
|
|
|
|
|
|
2019-01-26 12:53:11 -08:00
|
|
|
/**
|
|
|
|
|
* Callback invoked by the external api create or update a direct connection
|
|
|
|
|
* from the local client to an external client.
|
|
|
|
|
*
|
|
|
|
|
* @param {Object} event - The object containing information that should be
|
|
|
|
|
* passed to the {@code ProxyConnectionService}.
|
|
|
|
|
* @returns {void}
|
|
|
|
|
*/
|
|
|
|
|
onProxyConnectionEvent(event) {
|
|
|
|
|
if (!this._proxyConnection) {
|
|
|
|
|
this._proxyConnection = new JitsiMeetJS.ProxyConnectionService({
|
2019-03-13 13:15:56 -05:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Pass the {@code JitsiConnection} instance which will be used
|
|
|
|
|
* to fetch TURN credentials.
|
|
|
|
|
*/
|
|
|
|
|
jitsiConnection: APP.connection,
|
|
|
|
|
|
2019-01-26 12:53:11 -08:00
|
|
|
/**
|
|
|
|
|
* The proxy connection feature is currently tailored towards
|
|
|
|
|
* taking a proxied video stream and showing it as a local
|
|
|
|
|
* desktop screen.
|
|
|
|
|
*/
|
|
|
|
|
convertVideoToDesktop: true,
|
|
|
|
|
|
2019-07-29 15:21:53 -07:00
|
|
|
/**
|
|
|
|
|
* Callback invoked when the connection has been closed
|
|
|
|
|
* automatically. Triggers cleanup of screensharing if active.
|
|
|
|
|
*
|
|
|
|
|
* @returns {void}
|
|
|
|
|
*/
|
|
|
|
|
onConnectionClosed: () => {
|
|
|
|
|
if (this._untoggleScreenSharing) {
|
|
|
|
|
this._untoggleScreenSharing();
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2019-01-26 12:53:11 -08:00
|
|
|
/**
|
|
|
|
|
* Callback invoked to pass messages from the local client back
|
|
|
|
|
* out to the external client.
|
|
|
|
|
*
|
|
|
|
|
* @param {string} peerJid - The jid of the intended recipient
|
|
|
|
|
* of the message.
|
|
|
|
|
* @param {Object} data - The message that should be sent. For
|
|
|
|
|
* screensharing this is an iq.
|
|
|
|
|
* @returns {void}
|
|
|
|
|
*/
|
|
|
|
|
onSendMessage: (peerJid, data) =>
|
|
|
|
|
APP.API.sendProxyConnectionEvent({
|
|
|
|
|
data,
|
|
|
|
|
to: peerJid
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Callback invoked when the remote peer of the proxy connection
|
|
|
|
|
* has provided a video stream, intended to be used as a local
|
|
|
|
|
* desktop stream.
|
|
|
|
|
*
|
|
|
|
|
* @param {JitsiLocalTrack} remoteProxyStream - The media
|
|
|
|
|
* stream to use as a local desktop stream.
|
|
|
|
|
* @returns {void}
|
|
|
|
|
*/
|
|
|
|
|
onRemoteStream: desktopStream => {
|
|
|
|
|
if (desktopStream.videoType !== 'desktop') {
|
|
|
|
|
logger.warn('Received a non-desktop stream to proxy.');
|
|
|
|
|
desktopStream.dispose();
|
|
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-08 14:15:49 -05:00
|
|
|
APP.store.dispatch(toggleScreensharingA(undefined, false, { desktopStream }));
|
2019-01-26 12:53:11 -08:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this._proxyConnection.processMessage(event);
|
|
|
|
|
},
|
|
|
|
|
|
2017-08-04 11:15:11 +03:00
|
|
|
/**
|
|
|
|
|
* Sets the video muted status.
|
|
|
|
|
*/
|
2021-05-04 15:57:34 +03:00
|
|
|
setVideoMuteStatus() {
|
2020-10-23 17:48:56 -05:00
|
|
|
APP.UI.setVideoMuted(this.getMyUserId());
|
2017-08-04 11:15:11 +03:00
|
|
|
},
|
|
|
|
|
|
2018-01-19 14:19:55 -08:00
|
|
|
/**
|
|
|
|
|
* Dispatches the passed in feedback for submission. The submitted score
|
|
|
|
|
* should be a number inclusively between 1 through 5, or -1 for no score.
|
|
|
|
|
*
|
|
|
|
|
* @param {number} score - a number between 1 and 5 (inclusive) or -1 for no
|
|
|
|
|
* score.
|
|
|
|
|
* @param {string} message - An optional message to attach to the feedback
|
|
|
|
|
* in addition to the score.
|
|
|
|
|
* @returns {void}
|
|
|
|
|
*/
|
|
|
|
|
submitFeedback(score = -1, message = '') {
|
|
|
|
|
if (score === -1 || (score >= 1 && score <= 5)) {
|
|
|
|
|
APP.store.dispatch(submitFeedback(score, message, room));
|
|
|
|
|
}
|
2019-01-26 12:53:11 -08:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Terminates any proxy screensharing connection that is active.
|
|
|
|
|
*
|
|
|
|
|
* @private
|
|
|
|
|
* @returns {void}
|
|
|
|
|
*/
|
|
|
|
|
_stopProxyConnection() {
|
|
|
|
|
if (this._proxyConnection) {
|
|
|
|
|
this._proxyConnection.stop();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this._proxyConnection = null;
|
2017-08-18 12:30:30 +01:00
|
|
|
}
|
2016-07-07 20:44:04 -05:00
|
|
|
};
|