Compare commits
20 Commits
jitihouse/
...
release-66
Author | SHA1 | Date |
---|---|---|
Hristo Terezov | 24f3363d8e | |
Hristo Terezov | 0a11117b0b | |
Jaya Allamsetty | 911dd7fe57 | |
Jaya Allamsetty | 00cfe73561 | |
Hristo Terezov | 33a839572f | |
Hristo Terezov | 4b074f2327 | |
Дамян Минков | fe6267d51e | |
Jaya Allamsetty | f827d128bb | |
bogdandarie | 460f48194c | |
Avram Tudor | cb02edb20e | |
bogdandarie | 36e17466cb | |
bogdandarie | 7887f596aa | |
Calinteodor | cde552e69a | |
Hristo Terezov | 83c08d0679 | |
Hristo Terezov | 32fe70a08c | |
robertpin | f1c536697b | |
Saúl Ibarra Corretgé | 8b79f14431 | |
Jaya Allamsetty | a7f92557ac | |
Duduman Bogdan Vlad | 6d4d92b01d | |
Robert Pintilii | a31b2ea181 |
|
@ -127,6 +127,7 @@ import {
|
|||
isLocalTrackMuted,
|
||||
isUserInteractionRequiredForUnmute,
|
||||
replaceLocalTrack,
|
||||
toggleScreensharing as toggleScreensharingA,
|
||||
trackAdded,
|
||||
trackRemoved
|
||||
} from './react/features/base/tracks';
|
||||
|
@ -1733,6 +1734,8 @@ export default {
|
|||
* is not specified and starts the procedure for obtaining new screen
|
||||
* sharing/video track otherwise.
|
||||
*
|
||||
* NOTE: this is currently ONLY used in the non-multi-stream case.
|
||||
*
|
||||
* @param {boolean} [toggle] - If true - new screen sharing track will be
|
||||
* obtained. If false - new video track will be obtain. If not specified -
|
||||
* toggles between screen sharing and camera video.
|
||||
|
@ -2657,12 +2660,6 @@ export default {
|
|||
APP.UI.updateLargeVideo(displayedUserId, true);
|
||||
}
|
||||
});
|
||||
|
||||
APP.UI.addListener(
|
||||
UIEvents.TOGGLE_SCREENSHARING, ({ enabled, audioOnly, ignoreDidHaveVideo }) => {
|
||||
this.toggleScreenSharing(enabled, { audioOnly }, ignoreDidHaveVideo);
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -3216,7 +3213,7 @@ export default {
|
|||
return;
|
||||
}
|
||||
|
||||
this.toggleScreenSharing(undefined, { desktopStream });
|
||||
APP.store.dispatch(toggleScreensharingA(undefined, false, false, { desktopStream }));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
20
config.js
20
config.js
|
@ -221,9 +221,29 @@ var config = {
|
|||
// Specifies whether the raised hand will hide when someone becomes a dominant speaker or not
|
||||
// disableRemoveRaisedHandOnFocus: false,
|
||||
|
||||
// speakerStats: {
|
||||
// // Specifies whether the speaker stats is enable or not.
|
||||
// disabled: false,
|
||||
|
||||
// // Specifies whether there will be a search field in speaker stats or not.
|
||||
// disableSearch: false,
|
||||
|
||||
// // Specifies whether participants in speaker stats should be ordered or not, and with what priority.
|
||||
// // 'role', <- Moderators on top.
|
||||
// // 'name', <- Alphabetically by name.
|
||||
// // 'hasLeft', <- The ones that have left in the bottom.
|
||||
// order: [
|
||||
// 'role',
|
||||
// 'name',
|
||||
// 'hasLeft',
|
||||
// ],
|
||||
// },
|
||||
|
||||
// DEPRECATED. Please use speakerStats.disableSearch instead.
|
||||
// Specifies whether there will be a search field in speaker stats or not
|
||||
// disableSpeakerStatsSearch: false,
|
||||
|
||||
// DEPRECATED. Please use speakerStats.order .
|
||||
// Specifies whether participants in speaker stats should be ordered or not, and with what priority
|
||||
// speakerStatsOrder: [
|
||||
// 'role', <- Moderators on top
|
||||
|
|
|
@ -96,7 +96,7 @@
|
|||
"messageAccessibleTitleMe": "me says:",
|
||||
"messageTo": "Private message to {{recipient}}",
|
||||
"messagebox": "Type a message",
|
||||
"newMessages": "new messages",
|
||||
"newMessages": "New messages",
|
||||
"nickname": {
|
||||
"popover": "Choose a nickname",
|
||||
"title": "Enter a nickname to use chat",
|
||||
|
|
|
@ -30,11 +30,13 @@ import { toggleDialog } from '../../react/features/base/dialog/actions';
|
|||
import { isSupportedBrowser } from '../../react/features/base/environment';
|
||||
import { parseJWTFromURLParams } from '../../react/features/base/jwt';
|
||||
import JitsiMeetJS, { JitsiRecordingConstants } from '../../react/features/base/lib-jitsi-meet';
|
||||
import { MEDIA_TYPE } from '../../react/features/base/media';
|
||||
import { MEDIA_TYPE, VIDEO_TYPE } from '../../react/features/base/media';
|
||||
import {
|
||||
LOCAL_PARTICIPANT_DEFAULT_ID,
|
||||
getLocalParticipant,
|
||||
getParticipantById,
|
||||
getScreenshareParticipantIds,
|
||||
getVirtualScreenshareParticipantByOwnerId,
|
||||
grantModerator,
|
||||
hasRaisedHand,
|
||||
isLocalParticipantModerator,
|
||||
|
@ -234,13 +236,27 @@ function initCommands() {
|
|||
));
|
||||
}
|
||||
},
|
||||
'pin-participant': id => {
|
||||
'pin-participant': (id, videoType) => {
|
||||
logger.debug('Pin participant command received');
|
||||
|
||||
const state = APP.store.getState();
|
||||
const participant = videoType === VIDEO_TYPE.DESKTOP
|
||||
? getVirtualScreenshareParticipantByOwnerId(state, id) : getParticipantById(state, id);
|
||||
|
||||
if (!participant) {
|
||||
logger.warn('Trying to pin a non-existing participant with pin-participant command.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
sendAnalytics(createApiEvent('participant.pinned'));
|
||||
|
||||
const participantId = participant.id;
|
||||
|
||||
if (isStageFilmstripAvailable(APP.store.getState())) {
|
||||
APP.store.dispatch(addStageParticipant(id, true));
|
||||
APP.store.dispatch(addStageParticipant(participantId, true));
|
||||
} else {
|
||||
APP.store.dispatch(pinParticipant(id));
|
||||
APP.store.dispatch(pinParticipant(participantId));
|
||||
}
|
||||
},
|
||||
'proxy-connection-event': event => {
|
||||
|
@ -284,10 +300,29 @@ function initCommands() {
|
|||
|
||||
APP.store.dispatch(setFollowMe(value));
|
||||
},
|
||||
'set-large-video-participant': participantId => {
|
||||
'set-large-video-participant': (participantId, videoType) => {
|
||||
logger.debug('Set large video participant command received');
|
||||
|
||||
if (!participantId) {
|
||||
sendAnalytics(createApiEvent('largevideo.participant.set'));
|
||||
APP.store.dispatch(selectParticipantInLargeVideo());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const state = APP.store.getState();
|
||||
const participant = videoType === VIDEO_TYPE.DESKTOP
|
||||
? getVirtualScreenshareParticipantByOwnerId(state, participantId)
|
||||
: getParticipantById(state, participantId);
|
||||
|
||||
if (!participant) {
|
||||
logger.warn('Trying to select a non-existing participant with set-large-video-participant command.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
sendAnalytics(createApiEvent('largevideo.participant.set'));
|
||||
APP.store.dispatch(selectParticipantInLargeVideo(participantId));
|
||||
APP.store.dispatch(selectParticipantInLargeVideo(participant.id));
|
||||
},
|
||||
'set-participant-volume': (participantId, volume) => {
|
||||
APP.store.dispatch(setVolume(participantId, volume));
|
||||
|
@ -855,8 +890,7 @@ function initCommands() {
|
|||
callback(Boolean(APP.store.getState()['features/base/config'].startSilent));
|
||||
break;
|
||||
case 'get-content-sharing-participants': {
|
||||
const tracks = getState()['features/base/tracks'];
|
||||
const sharingParticipantIds = tracks.filter(tr => tr.videoType === 'desktop').map(t => t.participantId);
|
||||
const sharingParticipantIds = getScreenshareParticipantIds(APP.store.getState());
|
||||
|
||||
callback({
|
||||
sharingParticipantIds
|
||||
|
|
|
@ -1189,10 +1189,13 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
|
|||
*
|
||||
* @param {string} participantId - Participant id (JID) of the participant
|
||||
* that needs to be pinned on the stage view.
|
||||
* @param {string} [videoType] - Indicates the type of thumbnail to be pinned when multistream support is enabled.
|
||||
* Accepts "camera" or "desktop" values. Default is "camera". Any invalid values will be ignored and default will
|
||||
* be used.
|
||||
* @returns {void}
|
||||
*/
|
||||
pinParticipant(participantId) {
|
||||
this.executeCommand('pinParticipant', participantId);
|
||||
pinParticipant(participantId, videoType) {
|
||||
this.executeCommand('pinParticipant', participantId, videoType);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1283,10 +1286,13 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
|
|||
* the large video participant.
|
||||
*
|
||||
* @param {string} participantId - Jid of the participant to be displayed on the large video.
|
||||
* @param {string} [videoType] - Indicates the type of video to be set when multistream support is enabled.
|
||||
* Accepts "camera" or "desktop" values. Default is "camera". Any invalid values will be ignored and default will
|
||||
* be used.
|
||||
* @returns {void}
|
||||
*/
|
||||
setLargeVideoParticipant(participantId) {
|
||||
this.executeCommand('setLargeVideoParticipant', participantId);
|
||||
setLargeVideoParticipant(participantId, videoType) {
|
||||
this.executeCommand('setLargeVideoParticipant', participantId, videoType);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -34,7 +34,8 @@ import {
|
|||
isTrackStreamingStatusInactive,
|
||||
isTrackStreamingStatusInterrupted
|
||||
} from '../../../react/features/connection-indicator/functions';
|
||||
import { FILMSTRIP_BREAKPOINT, getVerticalViewMaxWidth, isFilmstripResizable } from '../../../react/features/filmstrip';
|
||||
import { FILMSTRIP_BREAKPOINT } from '../../../react/features/filmstrip/constants';
|
||||
import { getVerticalViewMaxWidth, isFilmstripResizable } from '../../../react/features/filmstrip/functions';
|
||||
import {
|
||||
updateKnownLargeVideoResolution
|
||||
} from '../../../react/features/large-video/actions';
|
||||
|
|
|
@ -12,7 +12,6 @@ import { toggleDialog } from '../../react/features/base/dialog';
|
|||
import { clickOnVideo } from '../../react/features/filmstrip/actions';
|
||||
import { KeyboardShortcutsDialog }
|
||||
from '../../react/features/keyboard-shortcuts';
|
||||
import { SpeakerStats } from '../../react/features/speaker-stats';
|
||||
|
||||
const logger = Logger.getLogger(__filename);
|
||||
|
||||
|
@ -249,13 +248,6 @@ const KeyboardShortcut = {
|
|||
});
|
||||
this._addShortcutToHelp('SPACE', 'keyboardShortcuts.pushToTalk');
|
||||
|
||||
this.registerShortcut('T', null, () => {
|
||||
sendAnalytics(createShortcutEvent('speaker.stats'));
|
||||
APP.store.dispatch(toggleDialog(SpeakerStats, {
|
||||
conference: APP.conference
|
||||
}));
|
||||
}, 'keyboardShortcuts.showSpeakerStats');
|
||||
|
||||
/**
|
||||
* FIXME: Currently focus keys are directly implemented below in
|
||||
* onkeyup. They should be moved to the SmallVideo instead.
|
||||
|
|
|
@ -74,7 +74,7 @@
|
|||
"js-md5": "0.6.1",
|
||||
"js-sha512": "0.8.0",
|
||||
"jwt-decode": "2.2.0",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1508.0.0+238dd7b2/lib-jitsi-meet.tgz",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet#3437c5426217e17fa8d8998cbd1de243dedc8399",
|
||||
"lodash": "4.17.21",
|
||||
"moment": "2.29.4",
|
||||
"moment-duration-format": "2.2.2",
|
||||
|
@ -13450,8 +13450,8 @@
|
|||
},
|
||||
"node_modules/lib-jitsi-meet": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1508.0.0+238dd7b2/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-FODZsk5ISwSVFAIwP6zu3IOdYL851dkSKQ4/aGaolTvErWZ2M69tIxEB4yAd81+9ewVL3nb9G0HGXJKFczTw1g==",
|
||||
"resolved": "git+ssh://git@github.com/jitsi/lib-jitsi-meet.git#3437c5426217e17fa8d8998cbd1de243dedc8399",
|
||||
"integrity": "sha512-ltlY6EJZAenekAFY54WOPKUHdYeIvg664heq4kITHip2CWR3QRkMrtn9pL3z8mtcMuuyu9p+a8gXXa2OBwEuaw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@jitsi/js-utils": "2.0.0",
|
||||
|
@ -30382,8 +30382,9 @@
|
|||
}
|
||||
},
|
||||
"lib-jitsi-meet": {
|
||||
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1508.0.0+238dd7b2/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-FODZsk5ISwSVFAIwP6zu3IOdYL851dkSKQ4/aGaolTvErWZ2M69tIxEB4yAd81+9ewVL3nb9G0HGXJKFczTw1g==",
|
||||
"version": "git+ssh://git@github.com/jitsi/lib-jitsi-meet.git#3437c5426217e17fa8d8998cbd1de243dedc8399",
|
||||
"integrity": "sha512-ltlY6EJZAenekAFY54WOPKUHdYeIvg664heq4kITHip2CWR3QRkMrtn9pL3z8mtcMuuyu9p+a8gXXa2OBwEuaw==",
|
||||
"from": "lib-jitsi-meet@https://github.com/jitsi/lib-jitsi-meet#3437c5426217e17fa8d8998cbd1de243dedc8399",
|
||||
"requires": {
|
||||
"@jitsi/js-utils": "2.0.0",
|
||||
"@jitsi/logger": "2.0.0",
|
||||
|
|
|
@ -79,7 +79,7 @@
|
|||
"js-md5": "0.6.1",
|
||||
"js-sha512": "0.8.0",
|
||||
"jwt-decode": "2.2.0",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1508.0.0+238dd7b2/lib-jitsi-meet.tgz",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet#3437c5426217e17fa8d8998cbd1de243dedc8399",
|
||||
"lodash": "4.17.21",
|
||||
"moment": "2.29.4",
|
||||
"moment-duration-format": "2.2.2",
|
||||
|
|
|
@ -20,6 +20,7 @@ import {
|
|||
parseURIString,
|
||||
parseURLParams
|
||||
} from '../base/util';
|
||||
import { inIframe } from '../base/util/iframeUtils';
|
||||
import { isVpaasMeeting } from '../jaas/functions';
|
||||
import {
|
||||
NOTIFICATION_TIMEOUT_TYPE,
|
||||
|
@ -164,7 +165,13 @@ export function maybeRedirectToWelcomePage(options: Object = {}) {
|
|||
// if close page is enabled redirect to it, without further action
|
||||
if (enableClosePage) {
|
||||
if (isVpaasMeeting(getState())) {
|
||||
dispatch(redirectToStaticPage('/'));
|
||||
const isOpenedInIframe = inIframe();
|
||||
|
||||
if (isOpenedInIframe) {
|
||||
window.location = 'about:blank';
|
||||
} else {
|
||||
dispatch(redirectToStaticPage('/'));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -186,9 +186,10 @@ export function getConferenceName(stateful: IStateful): string {
|
|||
const state = toState(stateful);
|
||||
const { callee } = state['features/base/jwt'];
|
||||
const { callDisplayName } = state['features/base/config'];
|
||||
const { localSubject, room, subject } = getConferenceState(state);
|
||||
const { localSubject, pendingSubjectChange, room, subject } = getConferenceState(state);
|
||||
|
||||
return (localSubject
|
||||
return (pendingSubjectChange
|
||||
|| localSubject
|
||||
|| subject
|
||||
|| callDisplayName
|
||||
|| callee?.name
|
||||
|
|
|
@ -135,7 +135,7 @@ function _conferenceFailed({ dispatch, getState }, next, action) {
|
|||
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
|
||||
|
||||
if (TRIGGER_READY_TO_CLOSE_REASONS.includes(reason)) {
|
||||
if (typeof APP === undefined) {
|
||||
if (typeof APP === 'undefined') {
|
||||
dispatch(readyToClose());
|
||||
} else {
|
||||
APP.API.notifyReadyToClose();
|
||||
|
|
|
@ -1,78 +1 @@
|
|||
// @flow
|
||||
|
||||
import { setPictureInPictureEnabled } from '../../mobile/picture-in-picture/functions';
|
||||
import { setAudioOnly } from '../audio-only';
|
||||
import JitsiMeetJS from '../lib-jitsi-meet';
|
||||
import { MiddlewareRegistry } from '../redux';
|
||||
import { TOGGLE_SCREENSHARING } from '../tracks/actionTypes';
|
||||
import { destroyLocalDesktopTrackIfExists, replaceLocalTrack } from '../tracks/actions';
|
||||
import { getLocalVideoTrack, isLocalVideoTrackDesktop } from '../tracks/functions';
|
||||
|
||||
import './middleware.any';
|
||||
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
switch (action.type) {
|
||||
case TOGGLE_SCREENSHARING: {
|
||||
_toggleScreenSharing(action.enabled, store);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return next(action);
|
||||
});
|
||||
|
||||
/**
|
||||
* Toggles screen sharing.
|
||||
*
|
||||
* @private
|
||||
* @param {boolean} enabled - The state to toggle screen sharing to.
|
||||
* @param {Store} store - The redux.
|
||||
* @returns {void}
|
||||
*/
|
||||
function _toggleScreenSharing(enabled, store) {
|
||||
const { dispatch, getState } = store;
|
||||
const state = getState();
|
||||
|
||||
if (enabled) {
|
||||
const isSharing = isLocalVideoTrackDesktop(state);
|
||||
|
||||
if (!isSharing) {
|
||||
_startScreenSharing(dispatch, state);
|
||||
}
|
||||
} else {
|
||||
dispatch(destroyLocalDesktopTrackIfExists());
|
||||
setPictureInPictureEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates desktop track and replaces the local one.
|
||||
*
|
||||
* @private
|
||||
* @param {Dispatch} dispatch - The redux {@code dispatch} function.
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {void}
|
||||
*/
|
||||
function _startScreenSharing(dispatch, state) {
|
||||
setPictureInPictureEnabled(false);
|
||||
|
||||
JitsiMeetJS.createLocalTracks({ devices: [ 'desktop' ] })
|
||||
.then(tracks => {
|
||||
const track = tracks[0];
|
||||
const currentLocalTrack = getLocalVideoTrack(state['features/base/tracks']);
|
||||
const currentJitsiTrack = currentLocalTrack && currentLocalTrack.jitsiTrack;
|
||||
|
||||
dispatch(replaceLocalTrack(currentJitsiTrack, track));
|
||||
|
||||
const { enabled: audioOnly } = state['features/base/audio-only'];
|
||||
|
||||
if (audioOnly) {
|
||||
dispatch(setAudioOnly(false));
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.log('ERROR creating ScreeSharing stream ', error);
|
||||
|
||||
setPictureInPictureEnabled(true);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,48 +1,14 @@
|
|||
// @flow
|
||||
|
||||
import { AUDIO_ONLY_SCREEN_SHARE_NO_TRACK } from '../../../../modules/UI/UIErrors';
|
||||
import UIEvents from '../../../../service/UI/UIEvents';
|
||||
import { showModeratedNotification } from '../../av-moderation/actions';
|
||||
import { shouldShowModeratedNotification } from '../../av-moderation/functions';
|
||||
import { setNoiseSuppressionEnabled } from '../../noise-suppression/actions';
|
||||
import {
|
||||
NOTIFICATION_TIMEOUT_TYPE,
|
||||
isModerationNotificationDisplayed,
|
||||
showNotification
|
||||
} from '../../notifications';
|
||||
import {
|
||||
setPrejoinPageVisibility,
|
||||
setSkipPrejoinOnReload
|
||||
} from '../../prejoin';
|
||||
import {
|
||||
isAudioOnlySharing,
|
||||
isScreenVideoShared,
|
||||
setScreenAudioShareState,
|
||||
setScreenshareAudioTrack
|
||||
} from '../../screen-share';
|
||||
import { isScreenshotCaptureEnabled, toggleScreenshotCaptureSummary } from '../../screenshot-capture';
|
||||
import { AudioMixerEffect } from '../../stream-effects/audio-mixer/AudioMixerEffect';
|
||||
import { setAudioOnly } from '../audio-only';
|
||||
import { getMultipleVideoSendingSupportFeatureFlag } from '../config/functions.any';
|
||||
import { JitsiConferenceErrors, JitsiTrackErrors, JitsiTrackEvents } from '../lib-jitsi-meet';
|
||||
import { MEDIA_TYPE, VIDEO_TYPE, setScreenshareMuted } from '../media';
|
||||
import { JitsiConferenceErrors } from '../lib-jitsi-meet';
|
||||
import { MiddlewareRegistry } from '../redux';
|
||||
import {
|
||||
TOGGLE_SCREENSHARING,
|
||||
addLocalTrack,
|
||||
createLocalTracksF,
|
||||
getLocalDesktopTrack,
|
||||
getLocalJitsiAudioTrack,
|
||||
replaceLocalTrack,
|
||||
toggleScreensharing
|
||||
} from '../tracks';
|
||||
|
||||
import { CONFERENCE_FAILED, CONFERENCE_JOINED, CONFERENCE_JOIN_IN_PROGRESS } from './actionTypes';
|
||||
import { getCurrentConference } from './functions';
|
||||
import './middleware.any';
|
||||
|
||||
declare var APP: Object;
|
||||
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
const { dispatch, getState } = store;
|
||||
const { enableForcedReload } = getState()['features/base/config'];
|
||||
|
@ -69,207 +35,7 @@ MiddlewareRegistry.register(store => next => action => {
|
|||
|
||||
break;
|
||||
}
|
||||
case TOGGLE_SCREENSHARING:
|
||||
if (typeof APP === 'object') {
|
||||
// check for A/V Moderation when trying to start screen sharing
|
||||
if ((action.enabled || action.enabled === undefined)
|
||||
&& shouldShowModeratedNotification(MEDIA_TYPE.VIDEO, store.getState())) {
|
||||
if (!isModerationNotificationDisplayed(MEDIA_TYPE.PRESENTER, store.getState())) {
|
||||
store.dispatch(showModeratedNotification(MEDIA_TYPE.PRESENTER));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const { enabled, audioOnly, ignoreDidHaveVideo } = action;
|
||||
|
||||
if (getMultipleVideoSendingSupportFeatureFlag(store.getState())) {
|
||||
_toggleScreenSharing(action, store);
|
||||
} else {
|
||||
APP.UI.emitEvent(UIEvents.TOGGLE_SCREENSHARING,
|
||||
{
|
||||
enabled,
|
||||
audioOnly,
|
||||
ignoreDidHaveVideo
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return next(action);
|
||||
});
|
||||
|
||||
/**
|
||||
* Displays a UI notification for screensharing failure based on the error passed.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} error - The error.
|
||||
* @param {Object} store - The redux store.
|
||||
* @returns {void}
|
||||
*/
|
||||
function _handleScreensharingError(error, { dispatch }) {
|
||||
if (error.name === JitsiTrackErrors.SCREENSHARING_USER_CANCELED) {
|
||||
return;
|
||||
}
|
||||
let descriptionKey, titleKey;
|
||||
|
||||
if (error.name === JitsiTrackErrors.PERMISSION_DENIED) {
|
||||
descriptionKey = 'dialog.screenSharingPermissionDeniedError';
|
||||
titleKey = 'dialog.screenSharingFailedTitle';
|
||||
} else if (error.name === JitsiTrackErrors.CONSTRAINT_FAILED) {
|
||||
descriptionKey = 'dialog.cameraConstraintFailedError';
|
||||
titleKey = 'deviceError.cameraError';
|
||||
} else if (error.name === JitsiTrackErrors.SCREENSHARING_GENERIC_ERROR) {
|
||||
descriptionKey = 'dialog.screenSharingFailed';
|
||||
titleKey = 'dialog.screenSharingFailedTitle';
|
||||
} else if (error === AUDIO_ONLY_SCREEN_SHARE_NO_TRACK) {
|
||||
descriptionKey = 'notify.screenShareNoAudio';
|
||||
titleKey = 'notify.screenShareNoAudioTitle';
|
||||
}
|
||||
|
||||
dispatch(showNotification({
|
||||
titleKey,
|
||||
descriptionKey
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the AudioMixer effect on the local audio track if applicable. If there is no local audio track, the desktop
|
||||
* audio track is added to the conference.
|
||||
*
|
||||
* @private
|
||||
* @param {JitsiLocalTrack} desktopAudioTrack - The audio track to be added to the conference.
|
||||
* @param {*} state - The redux state.
|
||||
* @returns {void}
|
||||
*/
|
||||
async function _maybeApplyAudioMixerEffect(desktopAudioTrack, state) {
|
||||
const localAudio = getLocalJitsiAudioTrack(state);
|
||||
const conference = getCurrentConference(state);
|
||||
|
||||
if (localAudio) {
|
||||
// If there is a localAudio stream, mix in the desktop audio stream captured by the screen sharing API.
|
||||
const mixerEffect = new AudioMixerEffect(desktopAudioTrack);
|
||||
|
||||
await localAudio.setEffect(mixerEffect);
|
||||
} else {
|
||||
// If no local stream is present ( i.e. no input audio devices) we use the screen share audio
|
||||
// stream as we would use a regular stream.
|
||||
await conference.replaceTrack(null, desktopAudioTrack);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles screen sharing.
|
||||
*
|
||||
* @private
|
||||
* @param {boolean} enabled - The state to toggle screen sharing to.
|
||||
* @param {Store} store - The redux store.
|
||||
* @returns {void}
|
||||
*/
|
||||
async function _toggleScreenSharing({ enabled, audioOnly = false, shareOptions = {} }, store) {
|
||||
const { dispatch, getState } = store;
|
||||
const state = getState();
|
||||
const audioOnlySharing = isAudioOnlySharing(state);
|
||||
const screenSharing = isScreenVideoShared(state);
|
||||
const conference = getCurrentConference(state);
|
||||
const localAudio = getLocalJitsiAudioTrack(state);
|
||||
const localScreenshare = getLocalDesktopTrack(state['features/base/tracks']);
|
||||
|
||||
// Toggle screenshare or audio-only share if the new state is not passed. Happens in the following two cases.
|
||||
// 1. ShareAudioDialog passes undefined when the user hits continue in the share audio demo modal.
|
||||
// 2. Toggle screenshare called from the external API.
|
||||
const enable = audioOnly
|
||||
? enabled ?? !audioOnlySharing
|
||||
: enabled ?? !screenSharing;
|
||||
const screensharingDetails = {};
|
||||
|
||||
if (enable) {
|
||||
let tracks;
|
||||
const options = {
|
||||
devices: [ VIDEO_TYPE.DESKTOP ],
|
||||
...shareOptions
|
||||
};
|
||||
|
||||
try {
|
||||
tracks = await createLocalTracksF(options);
|
||||
} catch (error) {
|
||||
_handleScreensharingError(error, store);
|
||||
|
||||
return;
|
||||
}
|
||||
const desktopAudioTrack = tracks.find(track => track.getType() === MEDIA_TYPE.AUDIO);
|
||||
const desktopVideoTrack = tracks.find(track => track.getType() === MEDIA_TYPE.VIDEO);
|
||||
|
||||
if (audioOnly) {
|
||||
// Dispose the desktop track for audio-only screensharing.
|
||||
desktopVideoTrack.dispose();
|
||||
|
||||
if (!desktopAudioTrack) {
|
||||
_handleScreensharingError(AUDIO_ONLY_SCREEN_SHARE_NO_TRACK, store);
|
||||
|
||||
return;
|
||||
}
|
||||
} else if (desktopVideoTrack) {
|
||||
if (localScreenshare) {
|
||||
await dispatch(replaceLocalTrack(localScreenshare.jitsiTrack, desktopVideoTrack, conference));
|
||||
} else {
|
||||
await dispatch(addLocalTrack(desktopVideoTrack));
|
||||
}
|
||||
if (isScreenshotCaptureEnabled(state, false, true)) {
|
||||
dispatch(toggleScreenshotCaptureSummary(true));
|
||||
}
|
||||
screensharingDetails.sourceType = desktopVideoTrack.sourceType;
|
||||
}
|
||||
|
||||
// Apply the AudioMixer effect if there is a local audio track, add the desktop track to the conference
|
||||
// otherwise without unmuting the microphone.
|
||||
if (desktopAudioTrack) {
|
||||
// Noise suppression doesn't work with desktop audio because we can't chain track effects yet, disable it
|
||||
// first. We need to to wait for the effect to clear first or it might interfere with the audio mixer.
|
||||
await dispatch(setNoiseSuppressionEnabled(false));
|
||||
_maybeApplyAudioMixerEffect(desktopAudioTrack, state);
|
||||
dispatch(setScreenshareAudioTrack(desktopAudioTrack));
|
||||
|
||||
// Handle the case where screen share was stopped from the browsers 'screen share in progress' window.
|
||||
if (audioOnly) {
|
||||
desktopAudioTrack?.on(
|
||||
JitsiTrackEvents.LOCAL_TRACK_STOPPED,
|
||||
() => dispatch(toggleScreensharing(undefined, true)));
|
||||
}
|
||||
}
|
||||
|
||||
// Disable audio-only or best performance mode if the user starts screensharing. This doesn't apply to
|
||||
// audio-only screensharing.
|
||||
const { enabled: bestPerformanceMode } = state['features/base/audio-only'];
|
||||
|
||||
if (bestPerformanceMode && !audioOnly) {
|
||||
dispatch(setAudioOnly(false));
|
||||
}
|
||||
} else {
|
||||
const { desktopAudioTrack } = state['features/screen-share'];
|
||||
|
||||
dispatch(toggleScreenshotCaptureSummary(false));
|
||||
|
||||
// Mute the desktop track instead of removing it from the conference since we don't want the client to signal
|
||||
// a source-remove to the remote peer for the screenshare track. Later when screenshare is enabled again, the
|
||||
// same sender will be re-used without the need for signaling a new ssrc through source-add.
|
||||
dispatch(setScreenshareMuted(true));
|
||||
if (desktopAudioTrack) {
|
||||
if (localAudio) {
|
||||
localAudio.setEffect(undefined);
|
||||
} else {
|
||||
await conference.replaceTrack(desktopAudioTrack, null);
|
||||
}
|
||||
desktopAudioTrack.dispose();
|
||||
dispatch(setScreenshareAudioTrack(null));
|
||||
}
|
||||
}
|
||||
|
||||
if (audioOnly) {
|
||||
dispatch(setScreenAudioShareState(enable));
|
||||
} else {
|
||||
// Notify the external API.
|
||||
APP.API.notifyScreenSharingStatusChanged(enable, screensharingDetails);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,6 +47,7 @@ export interface IJitsiConference {
|
|||
enableAVModeration: Function;
|
||||
getBreakoutRooms: Function;
|
||||
getLocalTracks: Function;
|
||||
getParticipants: Function;
|
||||
grantOwner: Function;
|
||||
isAVModerationSupported: Function;
|
||||
isEndConferenceSupported: Function;
|
||||
|
@ -58,6 +59,7 @@ export interface IJitsiConference {
|
|||
on: Function;
|
||||
removeTrack: Function;
|
||||
replaceTrack: Function;
|
||||
room: IJitsiConferenceRoom;
|
||||
sendCommand: Function;
|
||||
sendEndpointMessage: Function;
|
||||
sendLobbyMessage: Function;
|
||||
|
@ -93,6 +95,11 @@ export interface IConferenceState {
|
|||
subject?: string;
|
||||
}
|
||||
|
||||
export interface IJitsiConferenceRoom {
|
||||
myroomjid: string;
|
||||
roomjid: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen for actions that contain the conference object, so that it can be
|
||||
* stored for use by other action creators.
|
||||
|
|
|
@ -425,6 +425,11 @@ export interface IConfig {
|
|||
mode?: 'always' | 'recording';
|
||||
};
|
||||
serviceUrl?: string;
|
||||
speakerStats?: {
|
||||
disableSearch?: boolean;
|
||||
disabled?: boolean;
|
||||
order?: Array<'role' | 'name' | 'hasLeft'>;
|
||||
};
|
||||
speakerStatsOrder?: Array<'role' | 'name' | 'hasLeft'>;
|
||||
startAudioMuted?: number;
|
||||
startAudioOnly?: boolean;
|
||||
|
|
|
@ -210,6 +210,7 @@ export default [
|
|||
'resolution',
|
||||
'salesforceUrl',
|
||||
'screenshotCapture',
|
||||
'speakerStats',
|
||||
'startAudioMuted',
|
||||
'startAudioOnly',
|
||||
'startLastN',
|
||||
|
|
|
@ -449,6 +449,25 @@ function _translateLegacyConfig(oldValue: IConfig) {
|
|||
};
|
||||
}
|
||||
|
||||
newValue.speakerStats = newValue.speakerStats || {};
|
||||
|
||||
if (oldValue.disableSpeakerStatsSearch !== undefined
|
||||
&& newValue.speakerStats.disableSearch === undefined
|
||||
) {
|
||||
newValue.speakerStats = {
|
||||
...newValue.speakerStats,
|
||||
disableSearch: oldValue.disableSpeakerStatsSearch
|
||||
};
|
||||
}
|
||||
|
||||
if (oldValue.speakerStatsOrder !== undefined
|
||||
&& newValue.speakerStats.order === undefined) {
|
||||
newValue.speakerStats = {
|
||||
...newValue.speakerStats,
|
||||
order: oldValue.speakerStatsOrder
|
||||
};
|
||||
}
|
||||
|
||||
return newValue;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,32 +1,27 @@
|
|||
import { Theme } from '@mui/material';
|
||||
import { withStyles } from '@mui/styles';
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import Icon from '../../../icons/components/Icon';
|
||||
import { withPixelLineHeight } from '../../../styles/functions.web';
|
||||
import { COLORS } from '../../constants';
|
||||
import AbstractLabel, {
|
||||
type Props as AbstractProps
|
||||
} from '../AbstractLabel';
|
||||
|
||||
type Props = AbstractProps & {
|
||||
interface Props {
|
||||
|
||||
/**
|
||||
* Own CSS class name.
|
||||
*/
|
||||
className?: string;
|
||||
|
||||
/**
|
||||
* An object containing the CSS classes.
|
||||
*/
|
||||
classes: any;
|
||||
|
||||
/**
|
||||
* The color of the label.
|
||||
*/
|
||||
color?: string;
|
||||
|
||||
/**
|
||||
* An SVG icon to be rendered as the content of the label.
|
||||
*/
|
||||
icon?: Function;
|
||||
|
||||
/**
|
||||
* Color for the icon.
|
||||
|
@ -43,16 +38,14 @@ type Props = AbstractProps & {
|
|||
*/
|
||||
onClick?: (e?: React.MouseEvent) => void;
|
||||
|
||||
};
|
||||
/**
|
||||
* String or component that will be rendered as the label itself.
|
||||
*/
|
||||
text?: string;
|
||||
|
||||
/**
|
||||
* Creates the styles for the component.
|
||||
*
|
||||
* @param {Object} theme - The current UI theme.
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
const styles = (theme: Theme) => {
|
||||
}
|
||||
|
||||
const useStyles = makeStyles()((theme: Theme) => {
|
||||
return {
|
||||
label: {
|
||||
...withPixelLineHeight(theme.typography.labelRegular),
|
||||
|
@ -86,51 +79,33 @@ const styles = (theme: Theme) => {
|
|||
background: theme.palette.actionDanger
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const Label = ({
|
||||
className,
|
||||
color,
|
||||
icon,
|
||||
iconColor,
|
||||
id,
|
||||
onClick,
|
||||
text
|
||||
}: Props) => {
|
||||
const { classes, cx } = useStyles();
|
||||
|
||||
return (
|
||||
<div
|
||||
className = { cx(classes.label, onClick && classes.clickable,
|
||||
color && classes[color], className
|
||||
) }
|
||||
id = { id }
|
||||
onClick = { onClick }>
|
||||
{icon && <Icon
|
||||
color = { iconColor }
|
||||
size = '16'
|
||||
src = { icon } />}
|
||||
{text && <span className = { icon && classes.withIcon }>{text}</span>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* React Component for showing short text in a circle.
|
||||
*
|
||||
* @augments Component
|
||||
*/
|
||||
class Label extends AbstractLabel<Props, any> {
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const {
|
||||
classes,
|
||||
className,
|
||||
color,
|
||||
icon,
|
||||
iconColor,
|
||||
id,
|
||||
onClick,
|
||||
text
|
||||
} = this.props;
|
||||
const labelClassName = clsx(
|
||||
classes.label,
|
||||
onClick && classes.clickable,
|
||||
color && classes[color],
|
||||
className
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className = { labelClassName }
|
||||
id = { id }
|
||||
onClick = { onClick }>
|
||||
{ icon && <Icon
|
||||
color = { iconColor }
|
||||
size = '16'
|
||||
src = { icon } /> }
|
||||
{ text && <span className = { icon && classes.withIcon }>{text}</span> }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(Label);
|
||||
export default Label;
|
||||
|
|
|
@ -11,6 +11,7 @@ import { isCORSAvatarURL } from '../avatar/functions';
|
|||
import { getMultipleVideoSupportFeatureFlag, getSourceNameSignalingFeatureFlag } from '../config/functions.any';
|
||||
import i18next from '../i18n/i18next';
|
||||
import { JitsiParticipantConnectionStatus, JitsiTrackStreamingStatus } from '../lib-jitsi-meet';
|
||||
import { VIDEO_TYPE } from '../media/constants';
|
||||
import { shouldRenderVideoTrack } from '../media/functions';
|
||||
import { toState } from '../redux/functions';
|
||||
import { getScreenShareTrack, getVideoTrackByParticipant } from '../tracks/functions';
|
||||
|
@ -386,6 +387,19 @@ export function getScreenshareParticipantDisplayName(stateful: IStateful, id: st
|
|||
return i18next.t('screenshareDisplayName', { name: ownerDisplayName });
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of IDs of the participants that are currently screensharing.
|
||||
*
|
||||
* @param {(Function|Object)} stateful - The (whole) redux state, or redux's {@code getState} function to be used to
|
||||
* retrieve the state.
|
||||
* @returns {Array<string>}
|
||||
*/
|
||||
export function getScreenshareParticipantIds(stateful: IStateful): Array<string> {
|
||||
return toState(stateful)['features/base/tracks']
|
||||
.filter(track => track.videoType === VIDEO_TYPE.DESKTOP && !track.muted)
|
||||
.map(t => t.participantId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the presence status of a participant associated with the passed id.
|
||||
*
|
||||
|
|
|
@ -47,5 +47,9 @@ export interface LocalParticipant extends Participant {
|
|||
}
|
||||
|
||||
export interface IJitsiParticipant {
|
||||
getDisplayName: () => string;
|
||||
getId: () => string;
|
||||
getJid: () => string;
|
||||
getRole: () => string;
|
||||
isHidden: () => boolean;
|
||||
}
|
||||
|
|
|
@ -9,16 +9,6 @@
|
|||
*/
|
||||
export const SET_NO_SRC_DATA_NOTIFICATION_UID = 'SET_NO_SRC_DATA_NOTIFICATION_UID';
|
||||
|
||||
/**
|
||||
* The type of redux action dispatched to disable screensharing or to start the
|
||||
* flow for enabling screenshare.
|
||||
*
|
||||
* {
|
||||
* type: TOGGLE_SCREENSHARING
|
||||
* }
|
||||
*/
|
||||
export const TOGGLE_SCREENSHARING = 'TOGGLE_SCREENSHARING';
|
||||
|
||||
/**
|
||||
* The type of redux action dispatched when a track has been (locally or
|
||||
* remotely) added to the conference.
|
||||
|
|
|
@ -24,7 +24,6 @@ import { updateSettings } from '../settings/actions';
|
|||
|
||||
import {
|
||||
SET_NO_SRC_DATA_NOTIFICATION_UID,
|
||||
TOGGLE_SCREENSHARING,
|
||||
TRACK_ADDED,
|
||||
TRACK_CREATE_CANCELED,
|
||||
TRACK_CREATE_ERROR,
|
||||
|
@ -298,32 +297,6 @@ export function showNoDataFromSourceVideoError(jitsiTrack: any) {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals that the local participant is ending screensharing or beginning the screensharing flow.
|
||||
*
|
||||
* @param {boolean} enabled - The state to toggle screen sharing to.
|
||||
* @param {boolean} audioOnly - Only share system audio.
|
||||
* @param {boolean} ignoreDidHaveVideo - Whether or not to ignore if video was on when sharing started.
|
||||
* @param {Object} shareOptions - The options to be passed for capturing screenshare.
|
||||
* @returns {{
|
||||
* type: TOGGLE_SCREENSHARING,
|
||||
* on: boolean,
|
||||
* audioOnly: boolean,
|
||||
* ignoreDidHaveVideo: boolean,
|
||||
* shareOptions: Object
|
||||
* }}
|
||||
*/
|
||||
export function toggleScreensharing(enabled: boolean, audioOnly = false,
|
||||
ignoreDidHaveVideo = false, shareOptions = {}) {
|
||||
return {
|
||||
type: TOGGLE_SCREENSHARING,
|
||||
enabled,
|
||||
audioOnly,
|
||||
ignoreDidHaveVideo,
|
||||
shareOptions
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces one track with another for one renegotiation instead of invoking
|
||||
* two renegotiations with a separate removeTrack and addTrack. Disposes the
|
||||
|
@ -396,7 +369,7 @@ function replaceStoredTracks(oldTrack: any, newTrack: any) {
|
|||
* conference.
|
||||
*
|
||||
* @param {(JitsiLocalTrack|JitsiRemoteTrack)} track - JitsiTrack instance.
|
||||
* @returns {{ type: TRACK_ADDED, track: Track }}
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function trackAdded(track: any) {
|
||||
return async (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
|
@ -489,7 +462,13 @@ export function trackAdded(track: any) {
|
|||
* track: Track
|
||||
* }}
|
||||
*/
|
||||
export function trackMutedChanged(track: any) {
|
||||
export function trackMutedChanged(track: any): {
|
||||
track: {
|
||||
jitsiTrack: any;
|
||||
muted: boolean;
|
||||
};
|
||||
type: 'TRACK_UPDATED';
|
||||
} {
|
||||
return {
|
||||
type: TRACK_UPDATED,
|
||||
track: {
|
||||
|
@ -510,7 +489,11 @@ export function trackMutedChanged(track: any) {
|
|||
* track: Track
|
||||
* }}
|
||||
*/
|
||||
export function trackMuteUnmuteFailed(track: any, wasMuting: boolean) {
|
||||
export function trackMuteUnmuteFailed(track: any, wasMuting: boolean): {
|
||||
track: any;
|
||||
type: 'TRACK_MUTE_UNMUTE_FAILED';
|
||||
wasMuting: boolean;
|
||||
} {
|
||||
return {
|
||||
type: TRACK_MUTE_UNMUTE_FAILED,
|
||||
track,
|
||||
|
@ -548,7 +531,12 @@ export function trackNoDataFromSourceNotificationInfoChanged(track: any, noDataF
|
|||
* track: Track
|
||||
* }}
|
||||
*/
|
||||
export function trackRemoved(track: any) {
|
||||
export function trackRemoved(track: any): {
|
||||
track: {
|
||||
jitsiTrack: any;
|
||||
};
|
||||
type: 'TRACK_REMOVED';
|
||||
} {
|
||||
track.removeAllListeners(JitsiTrackEvents.TRACK_MUTE_CHANGED);
|
||||
track.removeAllListeners(JitsiTrackEvents.TRACK_VIDEOTYPE_CHANGED);
|
||||
track.removeAllListeners(JitsiTrackEvents.NO_DATA_FROM_SOURCE);
|
||||
|
@ -570,7 +558,13 @@ export function trackRemoved(track: any) {
|
|||
* track: Track
|
||||
* }}
|
||||
*/
|
||||
export function trackVideoStarted(track: any) {
|
||||
export function trackVideoStarted(track: any): {
|
||||
track: {
|
||||
jitsiTrack: any;
|
||||
videoStarted: true;
|
||||
};
|
||||
type: 'TRACK_UPDATED';
|
||||
} {
|
||||
return {
|
||||
type: TRACK_UPDATED,
|
||||
track: {
|
||||
|
@ -610,7 +604,13 @@ export function trackVideoTypeChanged(track: any, videoType: VideoType) {
|
|||
* track: Track
|
||||
* }}
|
||||
*/
|
||||
export function trackStreamingStatusChanged(track: any, streamingStatus: string) {
|
||||
export function trackStreamingStatusChanged(track: any, streamingStatus: string): {
|
||||
track: {
|
||||
jitsiTrack: any;
|
||||
streamingStatus: string;
|
||||
};
|
||||
type: 'TRACK_UPDATED';
|
||||
} {
|
||||
return {
|
||||
type: TRACK_UPDATED,
|
||||
track: {
|
||||
|
@ -643,7 +643,7 @@ function _addTracks(tracks: any[]) {
|
|||
* about here is to be sure that the {@code getUserMedia} callbacks have
|
||||
* completed (i.e. Returned from the native side).
|
||||
*/
|
||||
function _cancelGUMProcesses(getState: IStore['getState']) {
|
||||
function _cancelGUMProcesses(getState: IStore['getState']): Promise<any> {
|
||||
const logError
|
||||
= (error: Error) =>
|
||||
logger.error('gumProcess.cancel failed', JSON.stringify(error));
|
||||
|
@ -677,9 +677,9 @@ export function _disposeAndRemoveTracks(tracks: any[]) {
|
|||
* @returns {Promise} - A Promise resolved once {@link JitsiTrack.dispose()} is
|
||||
* done for every track from the list.
|
||||
*/
|
||||
function _disposeTracks(tracks: any) {
|
||||
function _disposeTracks(tracks: any[]): Promise<any> {
|
||||
return Promise.all(
|
||||
tracks.map((t: any) =>
|
||||
tracks.map(t =>
|
||||
t.dispose()
|
||||
.catch((err: Error) => {
|
||||
// Track might be already disposed so ignore such an error.
|
||||
|
@ -700,7 +700,7 @@ function _disposeTracks(tracks: any) {
|
|||
* @private
|
||||
* @returns {Function}
|
||||
*/
|
||||
function _onCreateLocalTracksRejected(error: Error, device: string) {
|
||||
function _onCreateLocalTracksRejected(error?: Error, device?: string) {
|
||||
return (dispatch: IStore['dispatch']) => {
|
||||
// If permissions are not allowed, alert the user.
|
||||
dispatch({
|
||||
|
@ -727,7 +727,7 @@ function _onCreateLocalTracksRejected(error: Error, device: string) {
|
|||
* @private
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function _shouldMirror(track: any) {
|
||||
function _shouldMirror(track: any): boolean {
|
||||
return (
|
||||
track?.isLocal()
|
||||
&& track?.isVideoTrack()
|
||||
|
@ -754,7 +754,10 @@ function _shouldMirror(track: any) {
|
|||
* trackType: MEDIA_TYPE
|
||||
* }}
|
||||
*/
|
||||
function _trackCreateCanceled(mediaType: MediaType) {
|
||||
function _trackCreateCanceled(mediaType: MediaType): {
|
||||
trackType: MediaType;
|
||||
type: 'TRACK_CREATE_CANCELED';
|
||||
} {
|
||||
return {
|
||||
type: TRACK_CREATE_CANCELED,
|
||||
trackType: mediaType
|
||||
|
@ -805,7 +808,11 @@ export function setNoSrcDataNotificationUid(uid?: string) {
|
|||
* name: string
|
||||
* }}
|
||||
*/
|
||||
export function updateLastTrackVideoMediaEvent(track: any, name: string) {
|
||||
export function updateLastTrackVideoMediaEvent(track: any, name: string): {
|
||||
name: string;
|
||||
track: any;
|
||||
type: 'TRACK_UPDATE_LAST_VIDEO_MEDIA_EVENT';
|
||||
} {
|
||||
return {
|
||||
type: TRACK_UPDATE_LAST_VIDEO_MEDIA_EVENT,
|
||||
track,
|
|
@ -0,0 +1,80 @@
|
|||
/* eslint-disable lines-around-comment */
|
||||
import { IState, IStore } from '../../app/types';
|
||||
// @ts-ignore
|
||||
import { setPictureInPictureEnabled } from '../../mobile/picture-in-picture/functions';
|
||||
// @ts-ignore
|
||||
import { setAudioOnly } from '../audio-only';
|
||||
import JitsiMeetJS from '../lib-jitsi-meet';
|
||||
|
||||
import { destroyLocalDesktopTrackIfExists, replaceLocalTrack } from './actions.any';
|
||||
// @ts-ignore
|
||||
import { getLocalVideoTrack, isLocalVideoTrackDesktop } from './functions';
|
||||
/* eslint-enable lines-around-comment */
|
||||
|
||||
export * from './actions.any';
|
||||
|
||||
/**
|
||||
* Signals that the local participant is ending screensharing or beginning the screensharing flow.
|
||||
*
|
||||
* @param {boolean} enabled - The state to toggle screen sharing to.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function toggleScreensharing(enabled: boolean): Function {
|
||||
return (store: IStore) => _toggleScreenSharing(enabled, store);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles screen sharing.
|
||||
*
|
||||
* @private
|
||||
* @param {boolean} enabled - The state to toggle screen sharing to.
|
||||
* @param {Store} store - The redux.
|
||||
* @returns {void}
|
||||
*/
|
||||
function _toggleScreenSharing(enabled: boolean, store: IStore): void {
|
||||
const { dispatch, getState } = store;
|
||||
const state = getState();
|
||||
|
||||
if (enabled) {
|
||||
const isSharing = isLocalVideoTrackDesktop(state);
|
||||
|
||||
if (!isSharing) {
|
||||
_startScreenSharing(dispatch, state);
|
||||
}
|
||||
} else {
|
||||
dispatch(destroyLocalDesktopTrackIfExists());
|
||||
setPictureInPictureEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates desktop track and replaces the local one.
|
||||
*
|
||||
* @private
|
||||
* @param {Dispatch} dispatch - The redux {@code dispatch} function.
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {void}
|
||||
*/
|
||||
function _startScreenSharing(dispatch: Function, state: IState) {
|
||||
setPictureInPictureEnabled(false);
|
||||
|
||||
JitsiMeetJS.createLocalTracks({ devices: [ 'desktop' ] })
|
||||
.then((tracks: any[]) => {
|
||||
const track = tracks[0];
|
||||
const currentLocalTrack = getLocalVideoTrack(state['features/base/tracks']);
|
||||
const currentJitsiTrack = currentLocalTrack?.jitsiTrack;
|
||||
|
||||
dispatch(replaceLocalTrack(currentJitsiTrack, track));
|
||||
|
||||
const { enabled: audioOnly } = state['features/base/audio-only'];
|
||||
|
||||
if (audioOnly) {
|
||||
dispatch(setAudioOnly(false));
|
||||
}
|
||||
})
|
||||
.catch((error: any) => {
|
||||
console.log('ERROR creating ScreeSharing stream ', error);
|
||||
|
||||
setPictureInPictureEnabled(true);
|
||||
});
|
||||
}
|
|
@ -0,0 +1,284 @@
|
|||
/* eslint-disable lines-around-comment */
|
||||
// @ts-ignore
|
||||
import { AUDIO_ONLY_SCREEN_SHARE_NO_TRACK } from '../../../../modules/UI/UIErrors';
|
||||
import { IState, IStore } from '../../app/types';
|
||||
import { showModeratedNotification } from '../../av-moderation/actions';
|
||||
import { shouldShowModeratedNotification } from '../../av-moderation/functions';
|
||||
import { setNoiseSuppressionEnabled } from '../../noise-suppression/actions';
|
||||
import { showNotification } from '../../notifications/actions';
|
||||
import { NOTIFICATION_TIMEOUT_TYPE } from '../../notifications/constants';
|
||||
import { isModerationNotificationDisplayed } from '../../notifications/functions';
|
||||
// @ts-ignore
|
||||
import { stopReceiver } from '../../remote-control/actions';
|
||||
// @ts-ignore
|
||||
import { setScreenAudioShareState, setScreenshareAudioTrack } from '../../screen-share/actions';
|
||||
import { isAudioOnlySharing, isScreenVideoShared } from '../../screen-share/functions';
|
||||
// @ts-ignore
|
||||
import { isScreenshotCaptureEnabled, toggleScreenshotCaptureSummary } from '../../screenshot-capture';
|
||||
// @ts-ignore
|
||||
import { AudioMixerEffect } from '../../stream-effects/audio-mixer/AudioMixerEffect';
|
||||
import { setAudioOnly } from '../audio-only/actions';
|
||||
import { getCurrentConference } from '../conference/functions';
|
||||
import { getMultipleVideoSendingSupportFeatureFlag } from '../config/functions.any';
|
||||
import { JitsiTrackErrors, JitsiTrackEvents } from '../lib-jitsi-meet';
|
||||
import { setScreenshareMuted } from '../media/actions';
|
||||
import { MEDIA_TYPE, VIDEO_TYPE } from '../media/constants';
|
||||
/* eslint-enable lines-around-comment */
|
||||
|
||||
import {
|
||||
addLocalTrack,
|
||||
replaceLocalTrack
|
||||
} from './actions.any';
|
||||
import {
|
||||
createLocalTracksF,
|
||||
getLocalDesktopTrack,
|
||||
getLocalJitsiAudioTrack
|
||||
} from './functions';
|
||||
import { ShareOptions, ToggleScreenSharingOptions } from './types';
|
||||
|
||||
export * from './actions.any';
|
||||
|
||||
declare const APP: any;
|
||||
|
||||
/**
|
||||
* Signals that the local participant is ending screensharing or beginning the screensharing flow.
|
||||
*
|
||||
* @param {boolean} enabled - The state to toggle screen sharing to.
|
||||
* @param {boolean} audioOnly - Only share system audio.
|
||||
* @param {boolean} ignoreDidHaveVideo - Whether or not to ignore if video was on when sharing started.
|
||||
* @param {Object} shareOptions - The options to be passed for capturing screenshare.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function toggleScreensharing(
|
||||
enabled?: boolean,
|
||||
audioOnly = false,
|
||||
ignoreDidHaveVideo = false,
|
||||
shareOptions: ShareOptions = {}) {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
// check for A/V Moderation when trying to start screen sharing
|
||||
if ((enabled || enabled === undefined)
|
||||
&& shouldShowModeratedNotification(MEDIA_TYPE.VIDEO, getState())) {
|
||||
if (!isModerationNotificationDisplayed(MEDIA_TYPE.PRESENTER, getState())) {
|
||||
dispatch(showModeratedNotification(MEDIA_TYPE.PRESENTER));
|
||||
}
|
||||
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
if (getMultipleVideoSendingSupportFeatureFlag(getState())) {
|
||||
return _toggleScreenSharing({
|
||||
enabled,
|
||||
audioOnly,
|
||||
shareOptions
|
||||
}, {
|
||||
dispatch,
|
||||
getState
|
||||
});
|
||||
}
|
||||
|
||||
return APP.conference.toggleScreenSharing(enabled, {
|
||||
audioOnly,
|
||||
desktopStream: shareOptions?.desktopStream
|
||||
}, ignoreDidHaveVideo);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a UI notification for screensharing failure based on the error passed.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} error - The error.
|
||||
* @param {Object} store - The redux store.
|
||||
* @returns {void}
|
||||
*/
|
||||
function _handleScreensharingError(
|
||||
error: Error | AUDIO_ONLY_SCREEN_SHARE_NO_TRACK,
|
||||
{ dispatch }: IStore): void {
|
||||
if (error.name === JitsiTrackErrors.SCREENSHARING_USER_CANCELED) {
|
||||
return;
|
||||
}
|
||||
let descriptionKey, titleKey;
|
||||
|
||||
if (error.name === JitsiTrackErrors.PERMISSION_DENIED) {
|
||||
descriptionKey = 'dialog.screenSharingPermissionDeniedError';
|
||||
titleKey = 'dialog.screenSharingFailedTitle';
|
||||
} else if (error.name === JitsiTrackErrors.CONSTRAINT_FAILED) {
|
||||
descriptionKey = 'dialog.cameraConstraintFailedError';
|
||||
titleKey = 'deviceError.cameraError';
|
||||
} else if (error.name === JitsiTrackErrors.SCREENSHARING_GENERIC_ERROR) {
|
||||
descriptionKey = 'dialog.screenSharingFailed';
|
||||
titleKey = 'dialog.screenSharingFailedTitle';
|
||||
} else if (error === AUDIO_ONLY_SCREEN_SHARE_NO_TRACK) {
|
||||
descriptionKey = 'notify.screenShareNoAudio';
|
||||
titleKey = 'notify.screenShareNoAudioTitle';
|
||||
}
|
||||
|
||||
dispatch(showNotification({
|
||||
titleKey,
|
||||
descriptionKey
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Applies the AudioMixer effect on the local audio track if applicable. If there is no local audio track, the desktop
|
||||
* audio track is added to the conference.
|
||||
*
|
||||
* @private
|
||||
* @param {JitsiLocalTrack} desktopAudioTrack - The audio track to be added to the conference.
|
||||
* @param {*} state - The redux state.
|
||||
* @returns {void}
|
||||
*/
|
||||
async function _maybeApplyAudioMixerEffect(desktopAudioTrack: any, state: IState): Promise<void> {
|
||||
const localAudio = getLocalJitsiAudioTrack(state);
|
||||
const conference = getCurrentConference(state);
|
||||
|
||||
if (localAudio) {
|
||||
// If there is a localAudio stream, mix in the desktop audio stream captured by the screen sharing API.
|
||||
const mixerEffect = new AudioMixerEffect(desktopAudioTrack);
|
||||
|
||||
await localAudio.setEffect(mixerEffect);
|
||||
} else {
|
||||
// If no local stream is present ( i.e. no input audio devices) we use the screen share audio
|
||||
// stream as we would use a regular stream.
|
||||
await conference.replaceTrack(null, desktopAudioTrack);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Toggles screen sharing.
|
||||
*
|
||||
* @private
|
||||
* @param {boolean} enabled - The state to toggle screen sharing to.
|
||||
* @param {Store} store - The redux store.
|
||||
* @returns {void}
|
||||
*/
|
||||
async function _toggleScreenSharing(
|
||||
{
|
||||
enabled,
|
||||
audioOnly = false,
|
||||
shareOptions = {}
|
||||
}: ToggleScreenSharingOptions,
|
||||
store: IStore
|
||||
): Promise<void> {
|
||||
const { dispatch, getState } = store;
|
||||
const state = getState();
|
||||
const audioOnlySharing = isAudioOnlySharing(state);
|
||||
const screenSharing = isScreenVideoShared(state);
|
||||
const conference = getCurrentConference(state);
|
||||
const localAudio = getLocalJitsiAudioTrack(state);
|
||||
const localScreenshare = getLocalDesktopTrack(state['features/base/tracks']);
|
||||
|
||||
// Toggle screenshare or audio-only share if the new state is not passed. Happens in the following two cases.
|
||||
// 1. ShareAudioDialog passes undefined when the user hits continue in the share audio demo modal.
|
||||
// 2. Toggle screenshare called from the external API.
|
||||
const enable = audioOnly
|
||||
? enabled ?? !audioOnlySharing
|
||||
: enabled ?? !screenSharing;
|
||||
const screensharingDetails: { sourceType?: string; } = {};
|
||||
|
||||
if (enable) {
|
||||
let tracks;
|
||||
|
||||
// Spot proxy stream.
|
||||
if (shareOptions.desktopStream) {
|
||||
tracks = [ shareOptions.desktopStream ];
|
||||
} else {
|
||||
const { _desktopSharingSourceDevice } = state['features/base/config'];
|
||||
|
||||
if (!shareOptions.desktopSharingSources && _desktopSharingSourceDevice) {
|
||||
shareOptions.desktopSharingSourceDevice = _desktopSharingSourceDevice;
|
||||
}
|
||||
|
||||
const options = {
|
||||
devices: [ VIDEO_TYPE.DESKTOP ],
|
||||
...shareOptions
|
||||
};
|
||||
|
||||
try {
|
||||
tracks = await createLocalTracksF(options) as any[];
|
||||
} catch (error) {
|
||||
_handleScreensharingError(error as any, store);
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const desktopAudioTrack = tracks.find(track => track.getType() === MEDIA_TYPE.AUDIO);
|
||||
const desktopVideoTrack = tracks.find(track => track.getType() === MEDIA_TYPE.VIDEO);
|
||||
|
||||
if (audioOnly) {
|
||||
// Dispose the desktop track for audio-only screensharing.
|
||||
desktopVideoTrack.dispose();
|
||||
|
||||
if (!desktopAudioTrack) {
|
||||
_handleScreensharingError(AUDIO_ONLY_SCREEN_SHARE_NO_TRACK, store);
|
||||
|
||||
throw new Error(AUDIO_ONLY_SCREEN_SHARE_NO_TRACK);
|
||||
}
|
||||
} else if (desktopVideoTrack) {
|
||||
if (localScreenshare) {
|
||||
await dispatch(replaceLocalTrack(localScreenshare.jitsiTrack, desktopVideoTrack, conference));
|
||||
} else {
|
||||
await dispatch(addLocalTrack(desktopVideoTrack));
|
||||
}
|
||||
if (isScreenshotCaptureEnabled(state, false, true)) {
|
||||
dispatch(toggleScreenshotCaptureSummary(true));
|
||||
}
|
||||
screensharingDetails.sourceType = desktopVideoTrack.sourceType;
|
||||
}
|
||||
|
||||
// Apply the AudioMixer effect if there is a local audio track, add the desktop track to the conference
|
||||
// otherwise without unmuting the microphone.
|
||||
if (desktopAudioTrack) {
|
||||
// Noise suppression doesn't work with desktop audio because we can't chain track effects yet, disable it
|
||||
// first. We need to to wait for the effect to clear first or it might interfere with the audio mixer.
|
||||
await dispatch(setNoiseSuppressionEnabled(false));
|
||||
_maybeApplyAudioMixerEffect(desktopAudioTrack, state);
|
||||
dispatch(setScreenshareAudioTrack(desktopAudioTrack));
|
||||
|
||||
// Handle the case where screen share was stopped from the browsers 'screen share in progress' window.
|
||||
if (audioOnly) {
|
||||
desktopAudioTrack?.on(
|
||||
JitsiTrackEvents.LOCAL_TRACK_STOPPED,
|
||||
() => dispatch(toggleScreensharing(undefined, true)));
|
||||
}
|
||||
}
|
||||
|
||||
// Disable audio-only or best performance mode if the user starts screensharing. This doesn't apply to
|
||||
// audio-only screensharing.
|
||||
const { enabled: bestPerformanceMode } = state['features/base/audio-only'];
|
||||
|
||||
if (bestPerformanceMode && !audioOnly) {
|
||||
dispatch(setAudioOnly(false));
|
||||
}
|
||||
} else {
|
||||
const { desktopAudioTrack } = state['features/screen-share'];
|
||||
|
||||
dispatch(stopReceiver());
|
||||
|
||||
dispatch(toggleScreenshotCaptureSummary(false));
|
||||
|
||||
// Mute the desktop track instead of removing it from the conference since we don't want the client to signal
|
||||
// a source-remove to the remote peer for the screenshare track. Later when screenshare is enabled again, the
|
||||
// same sender will be re-used without the need for signaling a new ssrc through source-add.
|
||||
dispatch(setScreenshareMuted(true));
|
||||
if (desktopAudioTrack) {
|
||||
if (localAudio) {
|
||||
localAudio.setEffect(undefined);
|
||||
} else {
|
||||
await conference.replaceTrack(desktopAudioTrack, null);
|
||||
}
|
||||
desktopAudioTrack.dispose();
|
||||
dispatch(setScreenshareAudioTrack(null));
|
||||
}
|
||||
}
|
||||
|
||||
if (audioOnly) {
|
||||
dispatch(setScreenAudioShareState(enable));
|
||||
} else {
|
||||
// Notify the external API.
|
||||
APP.API.notifyScreenSharingStatusChanged(enable, screensharingDetails);
|
||||
}
|
||||
}
|
|
@ -1,5 +1,3 @@
|
|||
// @flow
|
||||
|
||||
import { getLogger } from '../logging/functions';
|
||||
|
||||
export default getLogger('features/base/tracks');
|
||||
|
|
|
@ -42,6 +42,8 @@ import {
|
|||
trackMuteUnmuteFailed,
|
||||
trackNoDataFromSourceNotificationInfoChanged,
|
||||
trackRemoved
|
||||
|
||||
// @ts-ignore
|
||||
} from './actions';
|
||||
import {
|
||||
getLocalTrack,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import _ from 'lodash';
|
||||
|
||||
import { getScreenshareParticipantIds } from '../participants/functions';
|
||||
import StateListenerRegistry from '../redux/StateListenerRegistry';
|
||||
|
||||
import { isLocalCameraTrackMuted } from './functions';
|
||||
|
@ -8,8 +9,7 @@ import { isLocalCameraTrackMuted } from './functions';
|
|||
* Notifies when the list of currently sharing participants changes.
|
||||
*/
|
||||
StateListenerRegistry.register(
|
||||
/* selector */ state =>
|
||||
state['features/base/tracks'].filter(tr => tr.videoType === 'desktop').map(t => t.participantId),
|
||||
/* selector */ state => getScreenshareParticipantIds(state),
|
||||
/* listener */ (participantIDs, store, previousParticipantIDs) => {
|
||||
if (typeof APP !== 'object') {
|
||||
return;
|
||||
|
|
|
@ -17,3 +17,15 @@ export interface TrackOptions {
|
|||
micDeviceId?: string | null;
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
export interface ToggleScreenSharingOptions {
|
||||
audioOnly: boolean;
|
||||
enabled?: boolean;
|
||||
shareOptions: ShareOptions;
|
||||
}
|
||||
|
||||
export interface ShareOptions {
|
||||
desktopSharingSourceDevice?: string;
|
||||
desktopSharingSources?: string[];
|
||||
desktopStream?: any;
|
||||
}
|
||||
|
|
|
@ -1,14 +1,19 @@
|
|||
import _ from 'lodash';
|
||||
|
||||
import { IStateful } from '../base/app/types';
|
||||
// eslint-disable-next-line lines-around-comment
|
||||
// @ts-ignore
|
||||
import { getCurrentConference } from '../base/conference';
|
||||
import { getParticipantById, getParticipantCount, isLocalParticipantModerator } from '../base/participants/functions';
|
||||
import { getCurrentConference } from '../base/conference/functions';
|
||||
import { IJitsiConference } from '../base/conference/reducer';
|
||||
import {
|
||||
getLocalParticipant,
|
||||
getParticipantById,
|
||||
getParticipantCount,
|
||||
isLocalParticipantModerator
|
||||
} from '../base/participants/functions';
|
||||
import { IJitsiParticipant } from '../base/participants/types';
|
||||
import { toState } from '../base/redux/functions';
|
||||
|
||||
import { FEATURE_KEY } from './constants';
|
||||
import { IRoom, IRooms } from './types';
|
||||
import { IRoom, IRoomInfo, IRoomInfoParticipant, IRooms, IRoomsInfo } from './types';
|
||||
|
||||
/**
|
||||
* Returns the rooms object for breakout rooms.
|
||||
|
@ -32,9 +37,16 @@ export const getMainRoom = (stateful: IStateful) => {
|
|||
return _.find(rooms, room => Boolean(room.isMainRoom));
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the rooms info.
|
||||
*
|
||||
* @param {IStateful} stateful - The redux store, the redux.
|
||||
|
||||
* @returns {IRoomsInfo} The rooms info.
|
||||
*/
|
||||
export const getRoomsInfo = (stateful: IStateful) => {
|
||||
const breakoutRooms = getBreakoutRooms(stateful);
|
||||
const conference = getCurrentConference(stateful);
|
||||
const conference: IJitsiConference = getCurrentConference(stateful);
|
||||
|
||||
const initialRoomsInfo = {
|
||||
rooms: []
|
||||
|
@ -42,27 +54,45 @@ export const getRoomsInfo = (stateful: IStateful) => {
|
|||
|
||||
// only main roomn
|
||||
if (!breakoutRooms || Object.keys(breakoutRooms).length === 0) {
|
||||
// filter out hidden participants
|
||||
const conferenceParticipants = conference?.getParticipants()
|
||||
.filter((participant: IJitsiParticipant) => !participant.isHidden());
|
||||
|
||||
const localParticipant = getLocalParticipant(stateful);
|
||||
let localParticipantInfo;
|
||||
|
||||
if (localParticipant) {
|
||||
localParticipantInfo = {
|
||||
role: localParticipant.role,
|
||||
displayName: localParticipant.name,
|
||||
avatarUrl: localParticipant.loadableAvatarUrl,
|
||||
id: localParticipant.id
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...initialRoomsInfo,
|
||||
rooms: [ {
|
||||
isMainRoom: true,
|
||||
id: conference?.room?.roomjid,
|
||||
jid: conference?.room?.myroomjid,
|
||||
participants: conference?.participants && Object.keys(conference.participants).length
|
||||
? Object.keys(conference.participants).map(participantId => {
|
||||
const participantItem = conference?.participants[participantId];
|
||||
const storeParticipant = getParticipantById(stateful, participantItem._id);
|
||||
participants: conferenceParticipants?.length > 0
|
||||
? [
|
||||
localParticipantInfo,
|
||||
...conferenceParticipants.map((participantItem: IJitsiParticipant) => {
|
||||
const storeParticipant = getParticipantById(stateful, participantItem.getId());
|
||||
|
||||
return {
|
||||
jid: participantItem._jid,
|
||||
role: participantItem._role,
|
||||
displayName: participantItem._displayName,
|
||||
avatarUrl: storeParticipant?.loadableAvatarUrl,
|
||||
id: participantItem._id
|
||||
};
|
||||
}) : []
|
||||
} ]
|
||||
};
|
||||
return {
|
||||
jid: participantItem.getJid(),
|
||||
role: participantItem.getRole(),
|
||||
displayName: participantItem.getDisplayName(),
|
||||
avatarUrl: storeParticipant?.loadableAvatarUrl,
|
||||
id: participantItem.getId()
|
||||
} as IRoomInfoParticipant;
|
||||
}) ]
|
||||
: [ localParticipantInfo ]
|
||||
} as IRoomInfo ]
|
||||
} as IRoomsInfo;
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -88,11 +118,11 @@ export const getRoomsInfo = (stateful: IStateful) => {
|
|||
avatarUrl: storeParticipant?.loadableAvatarUrl,
|
||||
id: storeParticipant ? storeParticipant.id
|
||||
: participantLongId
|
||||
};
|
||||
} as IRoomInfoParticipant;
|
||||
}) : []
|
||||
};
|
||||
} as IRoomInfo;
|
||||
})
|
||||
};
|
||||
} as IRoomsInfo;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -15,3 +15,22 @@ export interface IRoom {
|
|||
export interface IRooms {
|
||||
[jid: string]: IRoom;
|
||||
}
|
||||
|
||||
export interface IRoomInfo {
|
||||
id: string;
|
||||
isMainRoom: boolean;
|
||||
jid: string;
|
||||
participants: IRoomInfoParticipant[];
|
||||
}
|
||||
|
||||
export interface IRoomsInfo {
|
||||
rooms: IRoomInfo[];
|
||||
}
|
||||
|
||||
export interface IRoomInfoParticipant {
|
||||
avatarUrl: string;
|
||||
displayName: string;
|
||||
id: string;
|
||||
jid: string;
|
||||
role: string;
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import { translate } from '../../../base/i18n/functions';
|
|||
import Icon from '../../../base/icons/components/Icon';
|
||||
import { IconArrowDown } from '../../../base/icons/svg';
|
||||
import { withPixelLineHeight } from '../../../base/styles/functions.web';
|
||||
import BaseTheme from '../../../base/ui/components/BaseTheme.web';
|
||||
|
||||
export interface INewMessagesButtonProps extends WithTranslation {
|
||||
|
||||
|
@ -29,7 +30,7 @@ const useStyles = makeStyles()((theme: Theme) => {
|
|||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
height: '32px',
|
||||
padding: '6px 8px',
|
||||
padding: '8px',
|
||||
border: 'none',
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
backgroundColor: theme.palette.action02,
|
||||
|
@ -49,12 +50,7 @@ const useStyles = makeStyles()((theme: Theme) => {
|
|||
width: '20px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingLeft: '5px',
|
||||
|
||||
'& svg': {
|
||||
fill: theme.palette.uiBackground
|
||||
}
|
||||
justifyContent: 'center'
|
||||
},
|
||||
|
||||
textContainer: {
|
||||
|
@ -83,9 +79,9 @@ function NewMessagesButton({ onGoToFirstUnreadMessage, t }: INewMessagesButtonPr
|
|||
type = 'button'>
|
||||
<Icon
|
||||
className = { styles.arrowDownIconContainer }
|
||||
color = { BaseTheme.palette.icon04 }
|
||||
size = { 14 }
|
||||
src = { IconArrowDown } />
|
||||
|
||||
<div className = { styles.textContainer }> { t('chat.newMessages') }</div>
|
||||
</button>
|
||||
</div>);
|
||||
|
|
|
@ -7,6 +7,8 @@ import emojiAsciiAliases from 'react-emoji-render/data/asciiAliases';
|
|||
import { IState } from '../app/types';
|
||||
import { escapeRegexp } from '../base/util/helpers';
|
||||
|
||||
import { IMessage } from './reducer';
|
||||
|
||||
/**
|
||||
* An ASCII emoticon regexp array to find and replace old-style ASCII
|
||||
* emoticons (such as :O) with the new Unicode representation, so that
|
||||
|
@ -93,14 +95,11 @@ export function getUnreadCount(state: IState) {
|
|||
}
|
||||
|
||||
let reactionMessages = 0;
|
||||
|
||||
if (!lastReadMessage) {
|
||||
return 0;
|
||||
}
|
||||
let lastReadIndex;
|
||||
|
||||
if (navigator.product === 'ReactNative') {
|
||||
// React native stores the messages in a reversed order.
|
||||
const lastReadIndex = messages.indexOf(lastReadMessage);
|
||||
lastReadIndex = messages.indexOf(<IMessage>lastReadMessage);
|
||||
|
||||
for (let i = 0; i < lastReadIndex; i++) {
|
||||
if (messages[i].isReaction) {
|
||||
|
@ -111,7 +110,7 @@ export function getUnreadCount(state: IState) {
|
|||
return lastReadIndex - reactionMessages;
|
||||
}
|
||||
|
||||
const lastReadIndex = messages.lastIndexOf(lastReadMessage);
|
||||
lastReadIndex = messages.lastIndexOf(<IMessage>lastReadMessage);
|
||||
|
||||
for (let i = lastReadIndex + 1; i < messagesCount; i++) {
|
||||
if (messages[i].isReaction) {
|
||||
|
@ -122,18 +121,6 @@ export function getUnreadCount(state: IState) {
|
|||
return messagesCount - (lastReadIndex + 1) - reactionMessages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector for calculating the number of unread chat messages.
|
||||
*
|
||||
* @param {IState} state - The redux state.
|
||||
* @returns {number} The number of unread messages.
|
||||
*/
|
||||
export function getUnreadMessagesCount(state: IState) {
|
||||
const { nbUnreadMessages } = state['features/chat'];
|
||||
|
||||
return nbUnreadMessages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether the chat smileys are disabled or not.
|
||||
*
|
||||
|
|
|
@ -21,7 +21,7 @@ import { Prejoin, isPrejoinPageVisible } from '../../../prejoin';
|
|||
import { toggleToolboxVisible } from '../../../toolbox/actions.any';
|
||||
import { fullScreenChanged, showToolbox } from '../../../toolbox/actions.web';
|
||||
import { JitsiPortal, Toolbox } from '../../../toolbox/components/web';
|
||||
import { LAYOUTS, getCurrentLayout } from '../../../video-layout';
|
||||
import { LAYOUT_CLASSNAMES, getCurrentLayout } from '../../../video-layout';
|
||||
import { maybeShowSuboptimalExperienceNotification } from '../../functions';
|
||||
import {
|
||||
AbstractConference,
|
||||
|
@ -48,20 +48,6 @@ const FULL_SCREEN_EVENTS = [
|
|||
'fullscreenchange'
|
||||
];
|
||||
|
||||
/**
|
||||
* The CSS class to apply to the root element of the conference so CSS can
|
||||
* modify the app layout.
|
||||
*
|
||||
* @private
|
||||
* @type {Object}
|
||||
*/
|
||||
export const LAYOUT_CLASSNAMES = {
|
||||
[LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW]: 'horizontal-filmstrip',
|
||||
[LAYOUTS.TILE_VIEW]: 'tile-view',
|
||||
[LAYOUTS.VERTICAL_FILMSTRIP_VIEW]: 'vertical-filmstrip',
|
||||
[LAYOUTS.STAGE_FILMSTRIP_VIEW]: 'stage-filmstrip'
|
||||
};
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link Conference}.
|
||||
*/
|
||||
|
|
|
@ -6,10 +6,10 @@ import type { Dispatch } from 'redux';
|
|||
import { openDialog } from '../../../base/dialog';
|
||||
import { IconUserGroups } from '../../../base/icons';
|
||||
import { Label } from '../../../base/label';
|
||||
import { COLORS } from '../../../base/label/constants';
|
||||
import { getParticipantCount } from '../../../base/participants';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { SpeakerStats } from '../../../speaker-stats';
|
||||
import { isSpeakerStatsDisabled } from '../../../speaker-stats/functions';
|
||||
|
||||
|
||||
/**
|
||||
|
@ -31,6 +31,11 @@ type Props = {
|
|||
* Invoked to open Speaker stats.
|
||||
*/
|
||||
dispatch: Dispatch<any>,
|
||||
|
||||
/**
|
||||
* Weather or not the speaker stats is disabled.
|
||||
*/
|
||||
_isSpeakerStatsDisabled: Boolean,
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -81,9 +86,8 @@ class ParticipantsCount extends PureComponent<Props> {
|
|||
|
||||
return (
|
||||
<Label
|
||||
color = { COLORS.white }
|
||||
icon = { IconUserGroups }
|
||||
onClick = { this._onClick }
|
||||
onClick = { !this.props._isSpeakerStatsDisabled && this._onClick }
|
||||
text = { count } />
|
||||
);
|
||||
}
|
||||
|
@ -101,7 +105,8 @@ class ParticipantsCount extends PureComponent<Props> {
|
|||
function mapStateToProps(state) {
|
||||
return {
|
||||
conference: state['features/base/conference'].conference,
|
||||
count: getParticipantCount(state)
|
||||
count: getParticipantCount(state),
|
||||
_isSpeakerStatsDisabled: isSpeakerStatsDisabled(state)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -2,8 +2,7 @@
|
|||
import React from 'react';
|
||||
|
||||
import { connect } from '../../../base/redux';
|
||||
import { LAYOUT_CLASSNAMES } from '../../../conference/components/web/Conference';
|
||||
import { LAYOUTS, getCurrentLayout } from '../../../video-layout';
|
||||
import { LAYOUTS, LAYOUT_CLASSNAMES, getCurrentLayout } from '../../../video-layout';
|
||||
import {
|
||||
FILMSTRIP_TYPE
|
||||
} from '../../constants';
|
||||
|
|
|
@ -4,8 +4,7 @@ import React from 'react';
|
|||
import { getToolbarButtons } from '../../../base/config';
|
||||
import { isMobileBrowser } from '../../../base/environment/utils';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { LAYOUT_CLASSNAMES } from '../../../conference/components/web/Conference';
|
||||
import { LAYOUTS, getCurrentLayout } from '../../../video-layout';
|
||||
import { LAYOUTS, LAYOUT_CLASSNAMES, getCurrentLayout } from '../../../video-layout';
|
||||
import {
|
||||
ASPECT_RATIO_BREAKPOINT,
|
||||
FILMSTRIP_TYPE,
|
||||
|
|
|
@ -26,17 +26,16 @@ import {
|
|||
} from '../../../base/participants/functions';
|
||||
import { Participant } from '../../../base/participants/types';
|
||||
import { ASPECT_RATIO_NARROW } from '../../../base/responsive-ui/constants';
|
||||
// @ts-ignore
|
||||
import { isTestModeEnabled } from '../../../base/testing';
|
||||
import { isTestModeEnabled } from '../../../base/testing/functions';
|
||||
import {
|
||||
getLocalAudioTrack,
|
||||
getLocalVideoTrack,
|
||||
getTrackByMediaTypeAndParticipant,
|
||||
getVirtualScreenshareParticipantTrack,
|
||||
trackStreamingStatusChanged,
|
||||
updateLastTrackVideoMediaEvent
|
||||
getVirtualScreenshareParticipantTrack
|
||||
// @ts-ignore
|
||||
} from '../../../base/tracks';
|
||||
// @ts-ignore
|
||||
import { trackStreamingStatusChanged, updateLastTrackVideoMediaEvent } from '../../../base/tracks/actions';
|
||||
import { getVideoObjectPosition } from '../../../face-landmarks/functions';
|
||||
// @ts-ignore
|
||||
import { hideGif, showGif } from '../../../gifs/actions';
|
||||
|
|
|
@ -7,6 +7,7 @@ import { MEDIA_TYPE } from '../base/media';
|
|||
import {
|
||||
getDominantSpeakerParticipant,
|
||||
getLocalParticipant,
|
||||
getLocalScreenShareParticipant,
|
||||
getPinnedParticipant,
|
||||
getRemoteParticipants,
|
||||
getVirtualScreenshareParticipantByOwnerId
|
||||
|
@ -138,7 +139,18 @@ function _electParticipantInLargeVideo(state) {
|
|||
return participant.id;
|
||||
}
|
||||
|
||||
if (getAutoPinSetting()) {
|
||||
const autoPinSetting = getAutoPinSetting();
|
||||
|
||||
if (autoPinSetting) {
|
||||
// when the setting auto_pin_latest_screen_share is true as spot does, prioritize local screenshare
|
||||
if (autoPinSetting === true) {
|
||||
const localScreenShareParticipant = getLocalScreenShareParticipant(state);
|
||||
|
||||
if (localScreenShareParticipant) {
|
||||
return localScreenShareParticipant.id;
|
||||
}
|
||||
}
|
||||
|
||||
// Pick the most recent remote screenshare that was added to the conference.
|
||||
const remoteScreenShares = state['features/video-layout'].remoteScreenShares;
|
||||
|
||||
|
|
|
@ -56,6 +56,13 @@ export const NOTIFICATION_ICON = {
|
|||
PARTICIPANTS: 'participants'
|
||||
};
|
||||
|
||||
/**
|
||||
* The identifier of the disable self view notification.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const DISABLE_SELF_VIEW_NOTIFICATION_ID = 'DISABLE_SELF_VIEW_NOTIFICATION_ID';
|
||||
|
||||
/**
|
||||
* The identifier of the lobby notification.
|
||||
*
|
||||
|
|
|
@ -228,12 +228,8 @@ const LocalRecordingManager: ILocalRecordingManager = {
|
|||
// @ts-ignore
|
||||
video: { displaySurface: 'browser',
|
||||
frameRate: 30 },
|
||||
audio: {
|
||||
autoGainControl: false,
|
||||
channelCount: 2,
|
||||
echoCancellation: false,
|
||||
noiseSuppression: false
|
||||
}
|
||||
audio: false,
|
||||
preferCurrentTab: true
|
||||
});
|
||||
document.title = currentTitle;
|
||||
|
||||
|
|
|
@ -222,31 +222,39 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => async action =>
|
|||
} else if (updatedSessionData.status !== PENDING) {
|
||||
dispatch(hidePendingRecordingNotification(mode));
|
||||
|
||||
if (updatedSessionData.status === ON
|
||||
&& (!oldSessionData || oldSessionData.status !== ON)) {
|
||||
if (typeof recordingLimit === 'object') {
|
||||
// Show notification with additional information to the initiator.
|
||||
dispatch(showRecordingLimitNotification(mode));
|
||||
} else {
|
||||
dispatch(showStartedRecordingNotification(mode, initiator, action.sessionData.id));
|
||||
if (updatedSessionData.status === ON) {
|
||||
|
||||
// We receive 2 updates of the session status ON. The first one is from jibri when it joins.
|
||||
// The second one is from jicofo which will deliever the initiator value. Since the start
|
||||
// recording notification uses the initiator value we skip the jibri update and show the
|
||||
// notification on the update from jicofo.
|
||||
// FIXE: simplify checks when the backend start sending only one status ON update containing the
|
||||
// initiator.
|
||||
if (initiator && (!oldSessionData || !oldSessionData.initiator)) {
|
||||
if (typeof recordingLimit === 'object') {
|
||||
dispatch(showRecordingLimitNotification(mode));
|
||||
} else {
|
||||
dispatch(showStartedRecordingNotification(mode, initiator, action.sessionData.id));
|
||||
}
|
||||
}
|
||||
if (!oldSessionData || oldSessionData.status !== ON) {
|
||||
sendAnalytics(createRecordingEvent('start', mode));
|
||||
|
||||
sendAnalytics(createRecordingEvent('start', mode));
|
||||
let soundID;
|
||||
|
||||
let soundID;
|
||||
if (mode === JitsiRecordingConstants.mode.FILE) {
|
||||
soundID = RECORDING_ON_SOUND_ID;
|
||||
} else if (mode === JitsiRecordingConstants.mode.STREAM) {
|
||||
soundID = LIVE_STREAMING_ON_SOUND_ID;
|
||||
}
|
||||
|
||||
if (mode === JitsiRecordingConstants.mode.FILE) {
|
||||
soundID = RECORDING_ON_SOUND_ID;
|
||||
} else if (mode === JitsiRecordingConstants.mode.STREAM) {
|
||||
soundID = LIVE_STREAMING_ON_SOUND_ID;
|
||||
}
|
||||
if (soundID) {
|
||||
dispatch(playSound(soundID));
|
||||
}
|
||||
|
||||
if (soundID) {
|
||||
dispatch(playSound(soundID));
|
||||
}
|
||||
|
||||
if (typeof APP !== 'undefined') {
|
||||
APP.API.notifyRecordingStatusChanged(true, mode);
|
||||
if (typeof APP !== 'undefined') {
|
||||
APP.API.notifyRecordingStatusChanged(true, mode);
|
||||
}
|
||||
}
|
||||
} else if (updatedSessionData.status === OFF
|
||||
&& (!oldSessionData || oldSessionData.status !== OFF)) {
|
||||
|
|
|
@ -5,9 +5,15 @@ import $ from 'jquery';
|
|||
import { getMultipleVideoSendingSupportFeatureFlag } from '../base/config/functions.any';
|
||||
import { openDialog } from '../base/dialog';
|
||||
import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
|
||||
import { getParticipantDisplayName, getPinnedParticipant, pinParticipant } from '../base/participants';
|
||||
import {
|
||||
getParticipantDisplayName,
|
||||
getPinnedParticipant,
|
||||
getVirtualScreenshareParticipantByOwnerId,
|
||||
pinParticipant
|
||||
} from '../base/participants';
|
||||
import { getLocalDesktopTrack, getLocalVideoTrack, toggleScreensharing } from '../base/tracks';
|
||||
import { NOTIFICATION_TIMEOUT_TYPE, showNotification } from '../notifications';
|
||||
import { isScreenVideoShared } from '../screen-share';
|
||||
|
||||
import {
|
||||
CAPTURE_EVENTS,
|
||||
|
@ -198,9 +204,12 @@ export function processPermissionRequestReply(participantId: string, event: Obje
|
|||
// the remote control permissions has been granted
|
||||
// pin the controlled participant
|
||||
const pinnedParticipant = getPinnedParticipant(state);
|
||||
const virtualScreenshareParticipantId = getVirtualScreenshareParticipantByOwnerId(state, participantId);
|
||||
const pinnedId = pinnedParticipant?.id;
|
||||
|
||||
if (pinnedId !== participantId) {
|
||||
if (virtualScreenshareParticipantId && pinnedId !== virtualScreenshareParticipantId) {
|
||||
dispatch(pinParticipant(virtualScreenshareParticipantId));
|
||||
} else if (!virtualScreenshareParticipantId && pinnedId !== participantId) {
|
||||
dispatch(pinParticipant(participantId));
|
||||
}
|
||||
}
|
||||
|
@ -508,6 +517,10 @@ export function sendStartRequest() {
|
|||
const { sourceId } = track?.jitsiTrack || {};
|
||||
const { transport } = state['features/remote-control'].receiver;
|
||||
|
||||
if (typeof sourceId === 'undefined') {
|
||||
return Promise.reject(new Error('Cannot identify screen for the remote control session'));
|
||||
}
|
||||
|
||||
return transport.sendRequest({
|
||||
name: REMOTE_CONTROL_MESSAGE_NAME,
|
||||
type: REQUESTS.start,
|
||||
|
@ -536,7 +549,7 @@ export function grant(participantId: string) {
|
|||
const tracks = state['features/base/tracks'];
|
||||
const isMultiStreamSupportEnabled = getMultipleVideoSendingSupportFeatureFlag(state);
|
||||
const track = isMultiStreamSupportEnabled ? getLocalDesktopTrack(tracks) : getLocalVideoTrack(tracks);
|
||||
const isScreenSharing = track?.videoType === 'desktop';
|
||||
const isScreenSharing = isScreenVideoShared(state);
|
||||
const { sourceType } = track?.jitsiTrack || {};
|
||||
|
||||
if (isScreenSharing && sourceType === 'screen') {
|
||||
|
|
|
@ -12,8 +12,9 @@ import {
|
|||
updateSettings
|
||||
// @ts-ignore
|
||||
} from '../../base/settings';
|
||||
// eslint-disable-next-line lines-around-comment
|
||||
// @ts-ignore
|
||||
import { toggleScreensharing } from '../../base/tracks';
|
||||
import { toggleScreensharing } from '../../base/tracks/actions';
|
||||
import Checkbox from '../../base/ui/components/web/Checkbox';
|
||||
|
||||
/**
|
||||
|
|
|
@ -6,7 +6,9 @@ import type { Dispatch } from 'redux';
|
|||
import { Dialog } from '../../base/dialog';
|
||||
import { translate } from '../../base/i18n';
|
||||
import { connect } from '../../base/redux';
|
||||
import { toggleScreensharing } from '../../base/tracks';
|
||||
// eslint-disable-next-line lines-around-comment
|
||||
// @ts-ignore
|
||||
import { toggleScreensharing } from '../../base/tracks/actions';
|
||||
|
||||
export type Props = {
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ import {
|
|||
|
||||
export interface IScreenShareState {
|
||||
captureFrameRate?: number;
|
||||
desktopAudioTrack?: Object;
|
||||
desktopAudioTrack?: any;
|
||||
isSharingAudio?: boolean;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { MiddlewareRegistry } from '../base/redux';
|
||||
import { SETTINGS_UPDATED, getHideSelfView } from '../base/settings';
|
||||
import { NOTIFICATION_TIMEOUT_TYPE, showNotification } from '../notifications';
|
||||
import { DISABLE_SELF_VIEW_NOTIFICATION_ID, NOTIFICATION_TIMEOUT_TYPE, showNotification } from '../notifications';
|
||||
|
||||
import { openSettingsDialog } from './actions';
|
||||
import { SETTINGS_TABS } from './constants';
|
||||
|
@ -16,6 +16,7 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
|||
|
||||
if (newValue !== oldValue && newValue) {
|
||||
dispatch(showNotification({
|
||||
uid: DISABLE_SELF_VIEW_NOTIFICATION_ID,
|
||||
titleKey: 'notify.selfViewTitle',
|
||||
customActionNameKey: [ 'settings.title' ],
|
||||
customActionHandler: [ () =>
|
||||
|
|
|
@ -14,7 +14,17 @@ import {
|
|||
* @returns {boolean} - True if the speaker stats search is disabled and false otherwise.
|
||||
*/
|
||||
export function isSpeakerStatsSearchDisabled(state: Object) {
|
||||
return state['features/base/config']?.disableSpeakerStatsSearch;
|
||||
return state['features/base/config']?.speakerStats.disableSearch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the speaker stats is disabled.
|
||||
*
|
||||
* @param {*} state - The redux state.
|
||||
* @returns {boolean} - True if the speaker stats search is disabled and false otherwise.
|
||||
*/
|
||||
export function isSpeakerStatsDisabled(state: Object) {
|
||||
return state['features/base/config']?.speakerStats?.disabled;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -24,7 +34,7 @@ export function isSpeakerStatsSearchDisabled(state: Object) {
|
|||
* @returns {Array<string>} - The speaker stats order array or an empty array.
|
||||
*/
|
||||
export function getSpeakerStatsOrder(state: Object) {
|
||||
return state['features/base/config']?.speakerStatsOrder ?? [
|
||||
return state['features/base/config']?.speakerStats.order ?? [
|
||||
'role',
|
||||
'name',
|
||||
'hasLeft'
|
||||
|
|
|
@ -15,6 +15,7 @@ import SecurityDialogButton
|
|||
from '../../../security/components/security-dialog/native/SecurityDialogButton';
|
||||
import { SharedVideoButton } from '../../../shared-video/components';
|
||||
import SpeakerStatsButton from '../../../speaker-stats/components/native/SpeakerStatsButton';
|
||||
import { isSpeakerStatsDisabled } from '../../../speaker-stats/functions';
|
||||
import { ClosedCaptionButton } from '../../../subtitles';
|
||||
import { TileViewButton } from '../../../video-layout';
|
||||
import styles from '../../../video-menu/components/native/styles';
|
||||
|
@ -54,7 +55,12 @@ type Props = {
|
|||
/**
|
||||
* Used for hiding the dialog when the selection was completed.
|
||||
*/
|
||||
dispatch: Function
|
||||
dispatch: Function,
|
||||
|
||||
/**
|
||||
* Whether or not speaker stats is disable.
|
||||
*/
|
||||
_isSpeakerStatsDisabled: boolean
|
||||
};
|
||||
|
||||
type State = {
|
||||
|
@ -95,6 +101,7 @@ class OverflowMenu extends PureComponent<Props, State> {
|
|||
*/
|
||||
render() {
|
||||
const {
|
||||
_isSpeakerStatsDisabled,
|
||||
_reactionsEnabled,
|
||||
_width
|
||||
} = this.props;
|
||||
|
@ -135,7 +142,7 @@ class OverflowMenu extends PureComponent<Props, State> {
|
|||
<Divider style = { styles.divider } />
|
||||
<SharedVideoButton { ...buttonProps } />
|
||||
{!toolbarButtons.has('screensharing') && <ScreenSharingButton { ...buttonProps } />}
|
||||
<SpeakerStatsButton { ...buttonProps } />
|
||||
{!_isSpeakerStatsDisabled && <SpeakerStatsButton { ...buttonProps } />}
|
||||
{!toolbarButtons.has('tileview') && <TileViewButton { ...buttonProps } />}
|
||||
<Divider style = { styles.divider } />
|
||||
<ClosedCaptionButton { ...buttonProps } />
|
||||
|
@ -176,6 +183,7 @@ class OverflowMenu extends PureComponent<Props, State> {
|
|||
*/
|
||||
function _mapStateToProps(state) {
|
||||
return {
|
||||
_isSpeakerStatsDisabled: isSpeakerStatsDisabled(state),
|
||||
_reactionsEnabled: isReactionsEnabled(state),
|
||||
_width: state['features/base/responsive-ui'].clientWidth
|
||||
};
|
||||
|
|
|
@ -6,6 +6,8 @@ import { batch } from 'react-redux';
|
|||
|
||||
// @ts-ignore
|
||||
import keyboardShortcut from '../../../../../modules/keyboardshortcut/keyboardshortcut';
|
||||
// @ts-ignore
|
||||
import { isSpeakerStatsDisabled } from '../../../../features/speaker-stats/functions';
|
||||
import { ACTION_SHORTCUT_TRIGGERED, createShortcutEvent, createToolbarEvent } from '../../../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../../../analytics/functions';
|
||||
import { IState } from '../../../app/types';
|
||||
|
@ -87,6 +89,7 @@ import { SettingsButton } from '../../../settings';
|
|||
import { SharedVideoButton } from '../../../shared-video/components';
|
||||
// @ts-ignore
|
||||
import { SpeakerStatsButton } from '../../../speaker-stats/components/web';
|
||||
import SpeakerStats from '../../../speaker-stats/components/web/SpeakerStats';
|
||||
import {
|
||||
ClosedCaptionButton
|
||||
// @ts-ignore
|
||||
|
@ -248,6 +251,12 @@ interface Props extends WithTranslation {
|
|||
_isProfileDisabled: boolean;
|
||||
|
||||
/**
|
||||
* Whether or not speaker stats is disable.
|
||||
*/
|
||||
_isSpeakerStatsDisabled: boolean;
|
||||
|
||||
|
||||
/**
|
||||
* Whether or not the current meeting belongs to a JaaS user.
|
||||
*/
|
||||
_isVpaasMeeting: boolean;
|
||||
|
@ -406,6 +415,7 @@ class Toolbox extends Component<Props> {
|
|||
this._onToolbarToggleRaiseHand = this._onToolbarToggleRaiseHand.bind(this);
|
||||
this._onToolbarToggleScreenshare = this._onToolbarToggleScreenshare.bind(this);
|
||||
this._onShortcutToggleTileView = this._onShortcutToggleTileView.bind(this);
|
||||
this._onShortcutSpeakerStats = this._onShortcutSpeakerStats.bind(this);
|
||||
this._onEscKey = this._onEscKey.bind(this);
|
||||
}
|
||||
|
||||
|
@ -416,7 +426,8 @@ class Toolbox extends Component<Props> {
|
|||
* @returns {void}
|
||||
*/
|
||||
componentDidMount() {
|
||||
const { _toolbarButtons, t, dispatch, _reactionsEnabled, _gifsEnabled } = this.props;
|
||||
const { _toolbarButtons, t, dispatch, _reactionsEnabled, _gifsEnabled, _isSpeakerStatsDisabled } = this.props;
|
||||
|
||||
const KEYBOARD_SHORTCUTS = [
|
||||
isToolbarButtonEnabled('videoquality', _toolbarButtons) && {
|
||||
character: 'A',
|
||||
|
@ -452,6 +463,11 @@ class Toolbox extends Component<Props> {
|
|||
character: 'W',
|
||||
exec: this._onShortcutToggleTileView,
|
||||
helpDescription: 'toolbar.tileViewToggle'
|
||||
},
|
||||
!_isSpeakerStatsDisabled && isToolbarButtonEnabled('stats', _toolbarButtons) && {
|
||||
character: 'T',
|
||||
exec: this._onShortcutSpeakerStats,
|
||||
helpDescription: 'keyboardShortcuts.showSpeakerStats'
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -723,9 +739,10 @@ class Toolbox extends Component<Props> {
|
|||
_getAllButtons() {
|
||||
const {
|
||||
_feedbackConfigured,
|
||||
_hasSalesforce,
|
||||
_isIosMobile,
|
||||
_isMobile,
|
||||
_hasSalesforce,
|
||||
_isSpeakerStatsDisabled,
|
||||
_multiStreamModeEnabled,
|
||||
_screenSharing,
|
||||
_whiteboardEnabled
|
||||
|
@ -889,7 +906,7 @@ class Toolbox extends Component<Props> {
|
|||
group: 3
|
||||
};
|
||||
|
||||
const speakerStats = {
|
||||
const speakerStats = !_isSpeakerStatsDisabled && {
|
||||
key: 'stats',
|
||||
Content: SpeakerStatsButton,
|
||||
group: 3
|
||||
|
@ -1229,6 +1246,34 @@ class Toolbox extends Component<Props> {
|
|||
this._doToggleScreenshare();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an analytics keyboard shortcut event and dispatches an action for
|
||||
* toggling speaker stats.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onShortcutSpeakerStats() {
|
||||
sendAnalytics(createShortcutEvent(
|
||||
'speaker.stats'
|
||||
));
|
||||
|
||||
this._doToggleSpekearStats();
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches an action to toggle speakerStats.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_doToggleSpekearStats() {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
dispatch(toggleDialog(SpeakerStats, {
|
||||
conference: APP.conference
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the toolbar visibility when tabbing into it.
|
||||
|
@ -1506,6 +1551,7 @@ class Toolbox extends Component<Props> {
|
|||
function _mapStateToProps(state: IState, ownProps: Partial<Props>) {
|
||||
const { conference } = state['features/base/conference'];
|
||||
const endConferenceSupported = conference?.isEndConferenceSupported();
|
||||
|
||||
const {
|
||||
buttonsWithNotifyClick,
|
||||
callStatsID,
|
||||
|
@ -1541,6 +1587,7 @@ function _mapStateToProps(state: IState, ownProps: Partial<Props>) {
|
|||
_isProfileDisabled: Boolean(disableProfile),
|
||||
_isIosMobile: isIosMobileBrowser(),
|
||||
_isMobile: isMobileBrowser(),
|
||||
_isSpeakerStatsDisabled: isSpeakerStatsDisabled(state),
|
||||
_isVpaasMeeting: isVpaasMeeting(state),
|
||||
_jwtDisabledButons: getJwtDisabledButtons(state),
|
||||
_hasSalesforce: isSalesforceEnabled(state),
|
||||
|
|
|
@ -9,3 +9,16 @@ export const LAYOUTS = {
|
|||
VERTICAL_FILMSTRIP_VIEW: 'vertical-filmstrip-view',
|
||||
STAGE_FILMSTRIP_VIEW: 'stage-filmstrip-view'
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* The CSS class to apply so CSS can modify the app layout.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
export const LAYOUT_CLASSNAMES = {
|
||||
[LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW]: 'horizontal-filmstrip',
|
||||
[LAYOUTS.TILE_VIEW]: 'tile-view',
|
||||
[LAYOUTS.VERTICAL_FILMSTRIP_VIEW]: 'vertical-filmstrip',
|
||||
[LAYOUTS.STAGE_FILMSTRIP_VIEW]: 'stage-filmstrip'
|
||||
};
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
getPinnedParticipant,
|
||||
pinParticipant
|
||||
} from '../base/participants';
|
||||
import { FakeParticipant } from '../base/participants/types';
|
||||
import { isStageFilmstripAvailable } from '../filmstrip/functions';
|
||||
import { isVideoPlaying } from '../shared-video/functions';
|
||||
import { VIDEO_QUALITY_LEVELS } from '../video-quality/constants';
|
||||
|
@ -124,7 +125,7 @@ export function updateAutoPinnedParticipant(
|
|||
const pinned = getPinnedParticipant(getState);
|
||||
|
||||
// if the pinned participant is shared video or some other fake participant we want to skip auto-pinning
|
||||
if (pinned?.isFakeParticipant) {
|
||||
if (pinned?.fakeParticipant && pinned.fakeParticipant !== FakeParticipant.RemoteScreenShare) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -40,7 +40,6 @@ export default {
|
|||
*/
|
||||
TOGGLE_FILMSTRIP: 'UI.toggle_filmstrip',
|
||||
|
||||
TOGGLE_SCREENSHARING: 'UI.toggle_screensharing',
|
||||
HANGUP: 'UI.hangup',
|
||||
LOGOUT: 'UI.logout',
|
||||
VIDEO_DEVICE_CHANGED: 'UI.video_device_changed',
|
||||
|
|
Loading…
Reference in New Issue