Compare commits

...

20 Commits

Author SHA1 Message Date
Hristo Terezov 24f3363d8e chore(LJM): update 2022-11-03 09:45:50 -05:00
Hristo Terezov 0a11117b0b Fix get rooms info (#12492)
* Include local participant; filter out hidden participants for getRoomsInfo

* Review fixes: include ts changes and types

Co-authored-by: Bogdan Duduman <bogdan.duduman@8x8.com>
2022-11-02 18:48:34 -05:00
Jaya Allamsetty 911dd7fe57 fix(video-layout) Fix auto-pinning latest screenshare. 2022-11-02 15:55:24 -04:00
Jaya Allamsetty 00cfe73561 fix(external-api) Remove muted SS tracks from the list of participants currently screensharing.
Fixes an issue where 'contentSharingParticipantsChanged' event and 'getContentSharingParticipants' API continue to list IDs of the participants that have already stopped their screenshares.
2022-11-02 12:43:26 -04:00
Hristo Terezov 33a839572f fix(iframeAPI): pinParticipant & setLargeVideo
Add the ability to specify video type when in multistream mode.
2022-11-02 10:46:24 -05:00
Hristo Terezov 4b074f2327 fix(recording): recording link.
Show the start recording notification on jicofo update only. This way
the initiator will be available and we will be able to fetch and display
recording link and send iframe API event.
2022-10-20 19:26:11 -05:00
Дамян Минков fe6267d51e fix: Drops participants count white background. (#12416)
* fix: Drops participants count white background.

Currently, it is white background with white icon.

* squash: Drop unused import.
2022-10-19 11:51:44 -05:00
Jaya Allamsetty f827d128bb chore(deps) Update lib-jitsi-meet.
3f0a4b7bdf
eb623e7536.
2022-10-18 11:40:11 -04:00
bogdandarie 460f48194c feat(config) add ability to hide speaker stats 2022-10-17 22:41:34 +02:00
Avram Tudor cb02edb20e fix(prejoin) show subject in prejoin if available (#12338) 2022-10-17 11:42:28 +03:00
bogdandarie 36e17466cb fix(jaas) redirect to about blank when close meeting opened in iframe 2022-10-17 09:35:40 +02:00
bogdandarie 7887f596aa fix(notifications) don't show self view notifications if already one is active 2022-10-17 09:35:26 +02:00
Calinteodor cde552e69a feat(chat): fixed chat counter (#12385)
* feat(chat): fixed chat counter and updated new messages button web styles
2022-10-14 17:11:30 +03:00
Hristo Terezov 83c08d0679 Fix TS error after cherry-pick in release branch 2022-10-13 13:43:50 -05:00
Hristo Terezov 32fe70a08c fix(remote-control): when multistream is enabled 2022-10-13 13:43:50 -05:00
robertpin f1c536697b fix(local-recording) don't use tab audio
We have observed that participant audio is distant and garbled, so we
added the tracks individually to the mixer.

In addition, using tab audio prevents us from using preferCurrentTab due
to: https://bugs.chromium.org/p/chromium/issues/detail?id=1317964 so
losing audio effects but having better participant audio quality (in
addition to better UX) is not a bad compromise.
2022-10-13 19:52:58 +02:00
Saúl Ibarra Corretgé 8b79f14431 fix(conference) fix Spot wireless screen sharing
Make sure we use the same screen-sharing flow which takes multi-stream
into consideration.
2022-10-13 15:34:08 +02:00
Jaya Allamsetty a7f92557ac fix(screenshare): Pass _desktopSharingSourceDevice as shareOptions when available.
Fixes an issue when external cam as screensharing source fails on Spot with multi-stream enabled.
2022-10-13 15:33:23 +02:00
Duduman Bogdan Vlad 6d4d92b01d fix(large-view) fix selection if local SS auto-select is true 2022-10-13 15:23:32 +02:00
Robert Pintilii a31b2ea181 ref(label) Convert to function component (#12370)
Fixes issue where Label styles would take precedence over parent styles (raised hand counter would be gray instead of yellow)
2022-10-13 10:47:37 +03:00
53 changed files with 875 additions and 591 deletions

View File

@ -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 }));
}
});
}

View File

@ -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

View File

@ -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",

View File

@ -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

View File

@ -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);
}
/**

View File

@ -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';

View File

@ -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.

11
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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;
}

View File

@ -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

View File

@ -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();

View File

@ -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);
});
}

View File

@ -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);
}
}

View File

@ -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.

View File

@ -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;

View File

@ -210,6 +210,7 @@ export default [
'resolution',
'salesforceUrl',
'screenshotCapture',
'speakerStats',
'startAudioMuted',
'startAudioOnly',
'startLastN',

View File

@ -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;
}

View File

@ -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;

View File

@ -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.
*

View File

@ -47,5 +47,9 @@ export interface LocalParticipant extends Participant {
}
export interface IJitsiParticipant {
getDisplayName: () => string;
getId: () => string;
getJid: () => string;
getRole: () => string;
isHidden: () => boolean;
}

View File

@ -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.

View File

@ -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,

View File

@ -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);
});
}

View File

@ -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);
}
}

View File

@ -1,5 +1,3 @@
// @flow
import { getLogger } from '../logging/functions';
export default getLogger('features/base/tracks');

View File

@ -42,6 +42,8 @@ import {
trackMuteUnmuteFailed,
trackNoDataFromSourceNotificationInfoChanged,
trackRemoved
// @ts-ignore
} from './actions';
import {
getLocalTrack,

View File

@ -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;

View File

@ -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;
}

View File

@ -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;
};
/**

View File

@ -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;
}

View File

@ -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>);

View File

@ -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.
*

View File

@ -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}.
*/

View File

@ -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)
};
}

View File

@ -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';

View File

@ -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,

View File

@ -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';

View File

@ -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;

View File

@ -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.
*

View File

@ -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;

View File

@ -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)) {

View File

@ -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') {

View File

@ -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';
/**

View File

@ -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 = {

View File

@ -9,7 +9,7 @@ import {
export interface IScreenShareState {
captureFrameRate?: number;
desktopAudioTrack?: Object;
desktopAudioTrack?: any;
isSharingAudio?: boolean;
}

View File

@ -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: [ () =>

View File

@ -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'

View File

@ -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
};

View File

@ -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),

View File

@ -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'
};

View File

@ -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;
}

View File

@ -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',