feat(multi-stream) Add fake participant tile for screen share.

prioritize participants with screen shares
support local screen share track
auto pin screen share
support screen share for large video
ensure fake screen share participants are sorted
fix local screen share in vertical filmstrip
fix local screen share in tile mode
use FakeScreenShareParticipant component for screen share thumbnails
ensure changes are behind feature flag and update jsdocs
fix bug where local screen share was not rendering
update receiver constraints to include SS source names
remove fake ss participant creation on track update
fix: handle screenshare muted change and track removal
refactor: update key values for sortedFakeScreenShareParticipants
address PR comments
refactor getter for screenshare tracks
rename state to sortedRemoteFakeScreenShareParticipants
This commit is contained in:
William Liang 2022-04-04 14:57:58 -04:00 committed by GitHub
parent 14d200a0cf
commit 70090fd716
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 851 additions and 79 deletions

View File

@ -48,7 +48,8 @@
/**
* The local video identifier.
*/
&#filmstripLocalVideo {
&#filmstripLocalVideo,
&#filmstripLocalScreenShare {
align-self: flex-end;
display: block;
margin-bottom: 8px;

View File

@ -2,7 +2,8 @@
* Various overrides outside of the filmstrip to style the app to support a
* tiled thumbnail experience.
*/
.tile-view, .stage-filmstrip {
.tile-view,
.stage-filmstrip {
/**
* Let the avatar grow with the tile.
*/

View File

@ -87,9 +87,27 @@
.videocontainer {
height: 0px;
width: 100%;
}
}
}
}
#filmstripLocalScreenShare {
align-self: initial;
margin-bottom: 5px;
display: flex;
flex-direction: column-reverse;
height: auto;
justify-content: flex-start;
width: 100%;
#filmstripLocalScreenShareThumbnail {
width: calc(100% - 15px);
.videocontainer {
height: 0px;
width: 100%;
}
}
}
/**
@ -97,6 +115,7 @@
* filmstrip from overlapping the left edge of the screen.
*/
#filmstripLocalVideo,
#filmstripLocalScreenShare,
.remote-videos {
padding: 0;
}

View File

@ -11,12 +11,14 @@ import { Avatar } from '../../../react/features/base/avatar';
import theme from '../../../react/features/base/components/themes/participantsPaneTheme.json';
import { getSourceNameSignalingFeatureFlag } from '../../../react/features/base/config';
import { i18next } from '../../../react/features/base/i18n';
import { MEDIA_TYPE, VIDEO_TYPE } from '../../../react/features/base/media';
import { VIDEO_TYPE } from '../../../react/features/base/media';
import {
getParticipantById,
getParticipantDisplayName
} from '../../../react/features/base/participants';
import { getTrackByMediaTypeAndParticipant } from '../../../react/features/base/tracks';
import {
getVideoTrackByParticipant
} from '../../../react/features/base/tracks';
import { CHAT_SIZE } from '../../../react/features/chat';
import {
isParticipantConnectionStatusActive,
@ -237,11 +239,14 @@ export default class LargeVideoManager {
let isVideoRenderable;
if (getSourceNameSignalingFeatureFlag(state)) {
const videoTrack = getTrackByMediaTypeAndParticipant(
state['features/base/tracks'], MEDIA_TYPE.VIDEO, id);
const tracks = state['features/base/tracks'];
const videoTrack = getVideoTrackByParticipant(tracks, participant);
isVideoRenderable = !isVideoMuted
&& (APP.conference.isLocalId(id) || isTrackStreamingStatusActive(videoTrack));
isVideoRenderable = !isVideoMuted && (
APP.conference.isLocalId(id)
|| participant?.isLocalScreenShare
|| isTrackStreamingStatusActive(videoTrack)
);
} else {
isVideoRenderable = !isVideoMuted
&& (APP.conference.isLocalId(id) || isParticipantConnectionStatusActive(participant));
@ -268,8 +273,10 @@ export default class LargeVideoManager {
&& participant && !participant.local && !participant.isFakeParticipant) {
// remote participant only
const track = getTrackByMediaTypeAndParticipant(
state['features/base/tracks'], MEDIA_TYPE.VIDEO, id);
const tracks = state['features/base/tracks'];
const track = getVideoTrackByParticipant(tracks, participant);
const isScreenSharing = track?.videoType === 'desktop';
if (isScreenSharing) {
@ -300,8 +307,8 @@ export default class LargeVideoManager {
let messageKey;
if (getSourceNameSignalingFeatureFlag(state)) {
const videoTrack = getTrackByMediaTypeAndParticipant(
state['features/base/tracks'], MEDIA_TYPE.VIDEO, id);
const tracks = state['features/base/tracks'];
const videoTrack = getVideoTrackByParticipant(tracks, participant);
messageKey = isTrackStreamingStatusInactive(videoTrack) ? 'connection.LOW_BANDWIDTH' : null;
} else {
@ -541,8 +548,8 @@ export default class LargeVideoManager {
const state = APP.store.getState();
if (getSourceNameSignalingFeatureFlag(state)) {
const videoTrack = getTrackByMediaTypeAndParticipant(
state['features/base/tracks'], MEDIA_TYPE.VIDEO, this.id);
const tracks = state['features/base/tracks'];
const videoTrack = getVideoTrackByParticipant(tracks, participant);
// eslint-disable-next-line no-param-reassign
show = !APP.conference.isLocalId(this.id)

View File

@ -2,12 +2,16 @@
import Logger from '@jitsi/logger';
import { getSourceNameSignalingFeatureFlag } from '../../../react/features/base/config';
import { MEDIA_TYPE, VIDEO_TYPE } from '../../../react/features/base/media';
import {
getPinnedParticipant,
getParticipantById
} from '../../../react/features/base/participants';
import { getTrackByMediaTypeAndParticipant } from '../../../react/features/base/tracks';
import {
getTrackByMediaTypeAndParticipant,
getFakeScreenshareParticipantTrack
} from '../../../react/features/base/tracks';
import LargeVideoManager from './LargeVideoManager';
import { VIDEO_CONTAINER_TYPE } from './VideoContainer';
@ -91,6 +95,10 @@ const VideoLayout = {
return VIDEO_TYPE.CAMERA;
}
if (getSourceNameSignalingFeatureFlag(state) && participant?.isFakeScreenShareParticipant) {
return VIDEO_TYPE.DESKTOP;
}
const videoTrack = getTrackByMediaTypeAndParticipant(state['features/base/tracks'], MEDIA_TYPE.VIDEO, id);
return videoTrack?.videoType;
@ -177,7 +185,17 @@ const VideoLayout = {
const currentContainerType = largeVideo.getCurrentContainerType();
const isOnLarge = this.isCurrentlyOnLarge(id);
const state = APP.store.getState();
const videoTrack = getTrackByMediaTypeAndParticipant(state['features/base/tracks'], MEDIA_TYPE.VIDEO, id);
const participant = getParticipantById(state, id);
const tracks = state['features/base/tracks'];
let videoTrack;
if (getSourceNameSignalingFeatureFlag(state) && participant?.isFakeScreenShareParticipant) {
videoTrack = getFakeScreenshareParticipantTrack(tracks, id);
} else {
videoTrack = getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, id);
}
const videoStream = videoTrack?.jitsiTrack;
if (isOnLarge && !forceUpdate

View File

@ -5,7 +5,11 @@ import debounce from 'lodash/debounce';
import { SET_FILMSTRIP_ENABLED } from '../../filmstrip/actionTypes';
import { SELECT_LARGE_VIDEO_PARTICIPANT } from '../../large-video/actionTypes';
import { APP_STATE_CHANGED } from '../../mobile/background/actionTypes';
import { SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED, SET_TILE_VIEW } from '../../video-layout/actionTypes';
import {
FAKE_SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED,
SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED,
SET_TILE_VIEW
} from '../../video-layout/actionTypes';
import { SET_AUDIO_ONLY } from '../audio-only/actionTypes';
import { CONFERENCE_JOINED } from '../conference/actionTypes';
import {
@ -92,6 +96,7 @@ MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case APP_STATE_CHANGED:
case CONFERENCE_JOINED:
case FAKE_SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED:
case PARTICIPANT_JOINED:
case PARTICIPANT_KICKED:
case PARTICIPANT_LEFT:

View File

@ -5,6 +5,7 @@ import type { Store } from 'redux';
import { isStageFilmstripEnabled } from '../../filmstrip/functions';
import { GRAVATAR_BASE_URL, isCORSAvatarURL } from '../avatar';
import { getSourceNameSignalingFeatureFlag } from '../config';
import { JitsiParticipantConnectionStatus } from '../lib-jitsi-meet';
import { MEDIA_TYPE, shouldRenderVideoTrack } from '../media';
import { toState } from '../redux';
@ -119,9 +120,11 @@ export function getNormalizedDisplayName(name: string) {
export function getParticipantById(
stateful: Object | Function, id: string): ?Object {
const state = toState(stateful)['features/base/participants'];
const { local, remote } = state;
const { local, localScreenShare, remote } = state;
return remote.get(id) || (local?.id === id ? local : undefined);
return remote.get(id)
|| (local?.id === id ? local : undefined)
|| (localScreenShare?.id === id ? localScreenShare : undefined);
}
/**
@ -148,10 +151,31 @@ export function getParticipantByIdOrUndefined(stateful: Object | Function, parti
* @returns {number}
*/
export function getParticipantCount(stateful: Object | Function) {
const state = toState(stateful)['features/base/participants'];
const { local, remote, fakeParticipants } = state;
const state = toState(stateful);
const {
local,
remote,
fakeParticipants,
sortedRemoteFakeScreenShareParticipants
} = state['features/base/participants'];
if (getSourceNameSignalingFeatureFlag(state)) {
return remote.size - fakeParticipants.size - sortedRemoteFakeScreenShareParticipants.size + (local ? 1 : 0);
}
return remote.size - fakeParticipants.size + (local ? 1 : 0);
}
/**
* Returns participant ID of the owner of a fake screenshare participant.
*
* @param {string} id - The ID of the fake screenshare participant.
* @private
* @returns {(string|undefined)}
*/
export function getFakeScreenShareParticipantOwnerId(id: string) {
return id.split('-')[0];
}
/**
@ -177,6 +201,10 @@ export function getFakeParticipants(stateful: Object | Function) {
export function getRemoteParticipantCount(stateful: Object | Function) {
const state = toState(stateful)['features/base/participants'];
if (getSourceNameSignalingFeatureFlag(state)) {
return state.remote.size - state.sortedRemoteFakeScreenShareParticipants.size;
}
return state.remote.size;
}
@ -190,8 +218,12 @@ export function getRemoteParticipantCount(stateful: Object | Function) {
* @returns {number}
*/
export function getParticipantCountWithFake(stateful: Object | Function) {
const state = toState(stateful)['features/base/participants'];
const { local, remote } = state;
const state = toState(stateful);
const { local, localScreenShare, remote } = state['features/base/participants'];
if (getSourceNameSignalingFeatureFlag(state)) {
return remote.size + (local ? 1 : 0) + (localScreenShare ? 1 : 0);
}
return remote.size + (local ? 1 : 0);
}

View File

@ -1,6 +1,8 @@
// @flow
import { SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED } from '../../video-layout/actionTypes';
import {
SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED
} from '../../video-layout/actionTypes';
import { ReducerRegistry, set } from '../redux';
import {
@ -59,9 +61,11 @@ const DEFAULT_STATE = {
fakeParticipants: new Map(),
haveParticipantWithScreenSharingFeature: false,
local: undefined,
localScreenShare: undefined,
pinnedParticipant: undefined,
raisedHandsQueue: [],
remote: new Map(),
sortedRemoteFakeScreenShareParticipants: new Map(),
sortedRemoteParticipants: new Map(),
sortedRemoteScreenshares: new Map(),
speakersList: new Map()
@ -207,7 +211,7 @@ ReducerRegistry.register('features/base/participants', (state = DEFAULT_STATE, a
}
case PARTICIPANT_JOINED: {
const participant = _participantJoined(action);
const { id, isFakeParticipant, name, pinned } = participant;
const { id, isFakeParticipant, isFakeScreenShareParticipant, isLocalScreenShare, name, pinned } = participant;
const { pinnedParticipant, dominantSpeaker } = state;
if (pinned) {
@ -241,6 +245,13 @@ ReducerRegistry.register('features/base/participants', (state = DEFAULT_STATE, a
};
}
if (isLocalScreenShare) {
return {
...state,
localScreenShare: participant
};
}
state.remote.set(id, participant);
// Insert the new participant.
@ -253,6 +264,14 @@ ReducerRegistry.register('features/base/participants', (state = DEFAULT_STATE, a
// The sort order of participants is preserved since Map remembers the original insertion order of the keys.
state.sortedRemoteParticipants = new Map(sortedRemoteParticipants);
if (isFakeScreenShareParticipant) {
const sortedRemoteFakeScreenShareParticipants = [ ...state.sortedRemoteFakeScreenShareParticipants ];
sortedRemoteFakeScreenShareParticipants.push([ id, name ]);
sortedRemoteFakeScreenShareParticipants.sort((a, b) => a[1].localeCompare(b[1]));
state.sortedRemoteFakeScreenShareParticipants = new Map(sortedRemoteFakeScreenShareParticipants);
}
if (isFakeParticipant) {
state.fakeParticipants.set(id, participant);
}
@ -267,7 +286,15 @@ ReducerRegistry.register('features/base/participants', (state = DEFAULT_STATE, a
// (and the fact that the local participant "joins" at the beginning of
// the app and "leaves" at the end of the app).
const { conference, id } = action.participant;
const { fakeParticipants, remote, local, dominantSpeaker, pinnedParticipant } = state;
const {
fakeParticipants,
sortedRemoteFakeScreenShareParticipants,
remote,
local,
localScreenShare,
dominantSpeaker,
pinnedParticipant
} = state;
let oldParticipant = remote.get(id);
if (oldParticipant && oldParticipant.conference === conference) {
@ -275,6 +302,9 @@ ReducerRegistry.register('features/base/participants', (state = DEFAULT_STATE, a
} else if (local?.id === id) {
oldParticipant = state.local;
delete state.local;
} else if (localScreenShare?.id === id) {
oldParticipant = state.local;
delete state.localScreenShare;
} else {
// no participant found
return state;
@ -324,6 +354,11 @@ ReducerRegistry.register('features/base/participants', (state = DEFAULT_STATE, a
fakeParticipants.delete(id);
}
if (sortedRemoteFakeScreenShareParticipants.has(id)) {
sortedRemoteFakeScreenShareParticipants.delete(id);
state.sortedRemoteFakeScreenShareParticipants = new Map(sortedRemoteFakeScreenShareParticipants);
}
return { ...state };
}
case RAISE_HAND_UPDATED: {
@ -447,6 +482,8 @@ function _participantJoined({ participant }) {
dominantSpeaker,
email,
isFakeParticipant,
isFakeScreenShareParticipant,
isLocalScreenShare,
isReplacing,
isJigasi,
loadableAvatarUrl,
@ -479,6 +516,8 @@ function _participantJoined({ participant }) {
email,
id,
isFakeParticipant,
isFakeScreenShareParticipant,
isLocalScreenShare,
isReplacing,
isJigasi,
loadableAvatarUrl,
@ -500,7 +539,7 @@ function _participantJoined({ participant }) {
* @returns {boolean} - True if a participant was updated and false otherwise.
*/
function _updateParticipantProperty(state, id, property, value) {
const { remote, local } = state;
const { remote, local, localScreenShare } = state;
if (remote.has(id)) {
remote.set(id, set(remote.get(id), property, value));
@ -511,6 +550,11 @@ function _updateParticipantProperty(state, id, property, value) {
// not in a conference.
state.local = set(local, property, value);
return true;
} else if (localScreenShare?.id === id) {
state.localScreenShare = set(localScreenShare, property, value);
return true;
}

View File

@ -107,6 +107,18 @@ export const TRACK_STOPPED = 'TRACK_STOPPED';
*/
export const TRACK_UPDATED = 'TRACK_UPDATED';
/**
* The type of redux action dispatched when a screenshare track's muted property were updated.
*
* {
* type: SCREENSHARE_TRACK_MUTED_UPDATED,
* track: Track,
* muted: Boolean
*
* }
*/
export const SCREENSHARE_TRACK_MUTED_UPDATED = 'SCREENSHARE_TRACK_MUTED_UPDATED';
/**
* The type of redux action dispatched when a local track starts being created
* via a WebRTC {@code getUserMedia} call. The action's payload includes an

View File

@ -6,7 +6,7 @@ import {
} from '../../analytics';
import { NOTIFICATION_TIMEOUT_TYPE, showErrorNotification, showNotification } from '../../notifications';
import { getCurrentConference } from '../conference';
import { getMultipleVideoSupportFeatureFlag } from '../config';
import { getMultipleVideoSupportFeatureFlag, getSourceNameSignalingFeatureFlag } from '../config';
import { JitsiTrackErrors, JitsiTrackEvents, createLocalTrack } from '../lib-jitsi-meet';
import {
CAMERA_FACING_MODE,
@ -21,6 +21,7 @@ import { getLocalParticipant } from '../participants';
import { updateSettings } from '../settings';
import {
SCREENSHARE_TRACK_MUTED_UPDATED,
SET_NO_SRC_DATA_NOTIFICATION_UID,
TOGGLE_SCREENSHARING,
TRACK_ADDED,
@ -395,7 +396,12 @@ export function trackAdded(track) {
return async (dispatch, getState) => {
track.on(
JitsiTrackEvents.TRACK_MUTE_CHANGED,
() => dispatch(trackMutedChanged(track)));
() => {
if (getSourceNameSignalingFeatureFlag(getState()) && track.getVideoType() === VIDEO_TYPE.DESKTOP) {
dispatch(screenshareTrackMutedChanged(track));
}
dispatch(trackMutedChanged(track));
});
track.on(
JitsiTrackEvents.TRACK_VIDEOTYPE_CHANGED,
type => dispatch(trackVideoTypeChanged(track, type)));
@ -491,6 +497,24 @@ export function trackMutedChanged(track) {
};
}
/**
* Create an action for when a screenshare track's muted state has been signaled to be changed.
*
* @param {(JitsiLocalTrack|JitsiRemoteTrack)} track - JitsiTrack instance.
* @returns {{
* type: TRACK_UPDATED,
* track: Track,
* muted: boolean
* }}
*/
export function screenshareTrackMutedChanged(track) {
return {
type: SCREENSHARE_TRACK_MUTED_UPDATED,
track: { jitsiTrack: track },
muted: track.isMuted()
};
}
/**
* Create an action for when a track's muted state change action has failed. This could happen because of
* {@code getUserMedia} errors during unmute or replace track errors at the peerconnection level.

View File

@ -4,6 +4,7 @@ import { getMultipleVideoSupportFeatureFlag } from '../config/functions.any';
import { isMobileBrowser } from '../environment/utils';
import JitsiMeetJS, { JitsiTrackErrors, browser } from '../lib-jitsi-meet';
import { MEDIA_TYPE, VIDEO_TYPE, setAudioMuted } from '../media';
import { getFakeScreenShareParticipantOwnerId } from '../participants';
import { toState } from '../redux';
import {
getUserSelectedCameraDeviceId,
@ -410,6 +411,35 @@ export function getLocalJitsiAudioTrack(state) {
return track?.jitsiTrack;
}
/**
* Returns track of specified media type for specified participant.
*
* @param {Track[]} tracks - List of all tracks.
* @param {Object} participant - Participant Object.
* @returns {(Track|undefined)}
*/
export function getVideoTrackByParticipant(
tracks,
participant) {
if (!participant) {
return;
}
let participantId;
let mediaType;
if (participant?.isFakeScreenShareParticipant) {
participantId = getFakeScreenShareParticipantOwnerId(participant.id);
mediaType = MEDIA_TYPE.SCREENSHARE;
} else {
participantId = participant.id;
mediaType = MEDIA_TYPE.VIDEO;
}
return getTrackByMediaTypeAndParticipant(tracks, mediaType, participantId);
}
/**
* Returns track of specified media type for specified participant id.
*
@ -427,6 +457,19 @@ export function getTrackByMediaTypeAndParticipant(
);
}
/**
* Returns track of given fakeScreenshareParticipantId.
*
* @param {Track[]} tracks - List of all tracks.
* @param {string} fakeScreenshareParticipantId - Fake Screenshare Participant ID.
* @returns {(Track|undefined)}
*/
export function getFakeScreenshareParticipantTrack(tracks, fakeScreenshareParticipantId) {
const participantId = getFakeScreenShareParticipantOwnerId(fakeScreenshareParticipantId);
return getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.SCREENSHARE, participantId);
}
/**
* Returns track source name of specified media type for specified participant id.
*

View File

@ -8,7 +8,7 @@ import { shouldShowModeratedNotification } from '../../av-moderation/functions';
import { hideNotification, isModerationNotificationDisplayed } from '../../notifications';
import { isPrejoinPageVisible } from '../../prejoin/functions';
import { getCurrentConference } from '../conference/functions';
import { getMultipleVideoSupportFeatureFlag } from '../config';
import { getMultipleVideoSupportFeatureFlag, getSourceNameSignalingFeatureFlag } from '../config';
import { getAvailableDevices } from '../devices/actions';
import {
CAMERA_FACING_MODE,
@ -24,9 +24,11 @@ import {
setScreenshareMuted,
SCREENSHARE_MUTISM_AUTHORITY
} from '../media';
import { participantLeft, participantJoined, getParticipantById } from '../participants';
import { MiddlewareRegistry, StateListenerRegistry } from '../redux';
import {
SCREENSHARE_TRACK_MUTED_UPDATED,
TOGGLE_SCREENSHARING,
TRACK_ADDED,
TRACK_MUTE_UNMUTE_FAILED,
@ -50,6 +52,7 @@ import {
isUserInteractionRequiredForUnmute,
setTrackMuted
} from './functions';
import logger from './logger';
import './subscriber';
@ -72,6 +75,13 @@ MiddlewareRegistry.register(store => next => action => {
store.dispatch(getAvailableDevices());
}
if (getSourceNameSignalingFeatureFlag(store.getState())
&& action.track.jitsiTrack.videoType === VIDEO_TYPE.DESKTOP
&& !action.track.jitsiTrack.isMuted()
) {
createFakeScreenShareParticipant(store, action);
}
break;
}
case TRACK_NO_DATA_FROM_SOURCE: {
@ -81,7 +91,40 @@ MiddlewareRegistry.register(store => next => action => {
return result;
}
case SCREENSHARE_TRACK_MUTED_UPDATED: {
const state = store.getState();
if (!getSourceNameSignalingFeatureFlag(state)) {
return;
}
const { track, muted } = action;
if (muted) {
const conference = getCurrentConference(state);
const participantId = track?.jitsiTrack.getSourceName();
store.dispatch(participantLeft(participantId, conference));
}
if (!muted) {
createFakeScreenShareParticipant(store, action);
}
break;
}
case TRACK_REMOVED: {
const state = store.getState();
if (getSourceNameSignalingFeatureFlag(state) && action.track.jitsiTrack.videoType === VIDEO_TYPE.DESKTOP) {
const conference = getCurrentConference(state);
const participantId = action.track.jitsiTrack.getSourceName();
store.dispatch(participantLeft(participantId, conference));
}
_removeNoDataFromSourceNotification(store, action.track);
break;
}
@ -326,6 +369,32 @@ function _handleNoDataFromSourceErrors(store, action) {
}
}
/**
* Creates a fake participant for screen share using the track's source name as the participant id.
*
* @param {Store} store - The redux store in which the specified action is dispatched.
* @param {Action} action - The redux action dispatched in the specified store.
* @private
* @returns {void}
*/
function createFakeScreenShareParticipant({ dispatch, getState }, { track }) {
const state = getState();
const participantId = track.jitsiTrack?.getParticipantId?.();
const participant = getParticipantById(state, participantId);
if (participant.name) {
dispatch(participantJoined({
conference: state['features/base/conference'].conference,
id: track.jitsiTrack.getSourceName(),
isFakeScreenShareParticipant: true,
isLocalScreenShare: track?.jitsiTrack.isLocal(),
name: `${participant.name}'s screen`
}));
} else {
logger.error(`Failed to create a screenshare participant for participantId: ${participantId}`);
}
}
/**
* Gets the local track associated with a specific {@code MEDIA_TYPE} in a
* specific redux store.

View File

@ -11,7 +11,9 @@ import { MEDIA_TYPE } from '../../../base/media';
import { getLocalParticipant, getParticipantById } from '../../../base/participants';
import { Popover } from '../../../base/popover';
import { connect } from '../../../base/redux';
import { getTrackByMediaTypeAndParticipant } from '../../../base/tracks';
import {
getFakeScreenshareParticipantTrack,
getTrackByMediaTypeAndParticipant } from '../../../base/tracks';
import {
isParticipantConnectionStatusInactive,
isParticipantConnectionStatusInterrupted,
@ -366,12 +368,18 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, State> {
*/
export function _mapStateToProps(state: Object, ownProps: Props) {
const { participantId } = ownProps;
const tracks = state['features/base/tracks'];
const sourceNameSignalingEnabled = getSourceNameSignalingFeatureFlag(state);
const firstVideoTrack = getTrackByMediaTypeAndParticipant(
state['features/base/tracks'], MEDIA_TYPE.VIDEO, participantId);
const participant = participantId ? getParticipantById(state, participantId) : getLocalParticipant(state);
let firstVideoTrack;
if (sourceNameSignalingEnabled && participant?.isFakeScreenShareParticipant) {
firstVideoTrack = getFakeScreenshareParticipantTrack(tracks, participantId);
} else {
firstVideoTrack = getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, participantId);
}
const _isConnectionStatusInactive = sourceNameSignalingEnabled
? isTrackStreamingStatusInactive(firstVideoTrack)
: isParticipantConnectionStatusInactive(participant);

View File

@ -87,6 +87,12 @@ type Props = AbstractProps & {
*/
_enableSaveLogs: boolean,
/**
* Whether or not the displays stats are for screen share. This prop is behind the sourceNameSignaling feature
* flag.
*/
_isFakeScreenShareParticipant: Boolean,
/**
* Whether or not the displays stats are for local video.
*/
@ -199,6 +205,7 @@ class ConnectionIndicatorContent extends AbstractConnectionIndicator<Props, Stat
e2eRtt = { e2eRtt }
enableSaveLogs = { this.props._enableSaveLogs }
framerate = { framerate }
isFakeScreenShareParticipant = { this.props._isFakeScreenShareParticipant }
isLocalVideo = { this.props._isLocalVideo }
maxEnabledResolution = { maxEnabledResolution }
onSaveLogs = { this.props._onSaveLogs }
@ -334,10 +341,11 @@ export function _mapStateToProps(state: Object, ownProps: Props) {
_connectionStatus: participant?.connectionStatus,
_enableSaveLogs: state['features/base/config'].enableSaveLogs,
_disableShowMoreStats: state['features/base/config'].disableShowMoreStats,
_isLocalVideo: participant?.local,
_region: participant?.region,
_isConnectionStatusInactive,
_isConnectionStatusInterrupted
_isConnectionStatusInterrupted,
_isFakeScreenShareParticipant: sourceNameSignalingEnabled && participant?.isFakeScreenShareParticipant,
_isLocalVideo: participant?.local,
_region: participant?.region
};
if (conference) {

View File

@ -91,6 +91,11 @@ type Props = {
*/
isLocalVideo: boolean,
/**
* Whether or not the statistics are for screen share.
*/
isFakeScreenShareParticipant: boolean,
/**
* The send-side max enabled resolution (aka the highest layer that is not
* suspended on the send-side).
@ -231,9 +236,19 @@ class ConnectionStatsTable extends Component<Props> {
* @returns {ReactElement}
*/
render() {
const { isLocalVideo, enableSaveLogs, disableShowMoreStats, classes } = this.props;
const {
classes,
disableShowMoreStats,
enableSaveLogs,
isFakeScreenShareParticipant,
isLocalVideo
} = this.props;
const className = clsx(classes.connectionStatsTable, { [classes.mobile]: isMobileBrowser() });
if (isFakeScreenShareParticipant) {
return this._renderScreenShareStatus();
}
return (
<ContextMenu
className = { classes.contextMenu }
@ -253,6 +268,34 @@ class ConnectionStatsTable extends Component<Props> {
);
}
/**
* Creates a ReactElement that will display connection statistics for a screen share thumbnail.
*
* @private
* @returns {ReactElement}
*/
_renderScreenShareStatus() {
const { classes } = this.props;
const className = isMobileBrowser() ? 'connection-info connection-info__mobile' : 'connection-info';
return (<ContextMenu
className = { classes.contextMenu }
hidden = { false }
inDrawer = { true }>
<div
className = { className }
onClick = { onClick }>
{ <table className = 'connection-info__container'>
<tbody>
{ this._renderResolution() }
{ this._renderFrameRate() }
</tbody>
</table> }
</div>
</ContextMenu>);
}
/**
* Creates a table as ReactElement that will display additional statistics
* related to bandwidth and transport for the local user.

View File

@ -1,6 +1,7 @@
// @flow
import type { Dispatch } from 'redux';
import { getSourceNameSignalingFeatureFlag } from '../base/config';
import {
getLocalParticipant,
getParticipantById,
@ -122,6 +123,7 @@ export function setVerticalViewDimensions() {
const resizableFilmstrip = isFilmstripResizable(state);
const _verticalViewGrid = showGridInVerticalView(state);
const numberOfRemoteParticipants = getRemoteParticipantCount(state);
const { localScreenShare } = state['features/base/participants'];
let gridView = {};
let thumbnails = {};
@ -179,6 +181,20 @@ export function setVerticalViewDimensions() {
= thumbnails?.local?.width + TILE_VERTICAL_CONTAINER_HORIZONTAL_MARGIN + SCROLL_SIZE;
remoteVideosContainerHeight
= clientHeight - (disableSelfView ? 0 : thumbnails?.local?.height) - VERTICAL_FILMSTRIP_VERTICAL_MARGIN;
if (getSourceNameSignalingFeatureFlag(state)) {
// Account for the height of the local screen share thumbnail when calculating the height of the remote
// videos container.
const localCameraThumbnailHeight = thumbnails?.local?.height;
const localScreenShareThumbnailHeight
= localScreenShare && !disableSelfView ? thumbnails?.local?.height : 0;
remoteVideosContainerHeight = clientHeight
- localCameraThumbnailHeight
- localScreenShareThumbnailHeight
- VERTICAL_FILMSTRIP_VERTICAL_MARGIN;
}
hasScroll
= remoteVideosContainerHeight
< (thumbnails?.remote.height + TILE_VERTICAL_MARGIN) * numberOfRemoteParticipants;

View File

@ -0,0 +1,157 @@
// @flow
import clsx from 'clsx';
import React from 'react';
import { useSelector } from 'react-redux';
import { VideoTrack } from '../../../base/media';
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
import ThumbnailBottomIndicators from './ThumbnailBottomIndicators';
import ThumbnailTopIndicators from './ThumbnailTopIndicators';
type Props = {
/**
* An object containing the CSS classes.
*/
classes: Object,
/**
* The class name that will be used for the container.
*/
containerClassName: string,
/**
* Indicates whether the thumbnail is hovered or not.
*/
isHovered: boolean,
/**
* Indicates whether we are currently running in a mobile browser.
*/
isMobile: boolean,
/**
* Click handler.
*/
onClick: Function,
/**
* Mouse enter handler.
*/
onMouseEnter: Function,
/**
* Mouse leave handler.
*/
onMouseLeave: Function,
/**
* Mouse move handler.
*/
onMouseMove: Function,
/**
* Touch end handler.
*/
onTouchEnd: Function,
/**
* Touch move handler.
*/
onTouchMove: Function,
/**
* Touch start handler.
*/
onTouchStart: Function,
/**
* The ID of the fake screen share participant.
*/
participantId: string,
/**
* An object with the styles for thumbnail.
*/
styles: Object,
/**
* JitsiTrack instance.
*/
videoTrack: Object
}
const FakeScreenShareParticipant = ({
classes,
containerClassName,
isHovered,
isMobile,
onClick,
onMouseEnter,
onMouseLeave,
onMouseMove,
onTouchEnd,
onTouchMove,
onTouchStart,
participantId,
styles,
videoTrack
}: Props) => {
const currentLayout = useSelector(getCurrentLayout);
const videoTrackId = videoTrack?.jitsiTrack?.getId();
const video = videoTrack && <VideoTrack
id = { `remoteVideo_${videoTrackId || ''}` }
muted = { true }
style = { styles.video }
videoTrack = { videoTrack } />;
return (
<span
className = { containerClassName }
id = { `participant_${participantId}` }
{ ...(isMobile
? {
onTouchEnd,
onTouchMove,
onTouchStart
}
: {
onClick,
onMouseEnter,
onMouseMove,
onMouseLeave
}
) }
style = { styles.thumbnail }>
{video}
<div className = { classes.containerBackground } />
<div
className = { clsx(classes.indicatorsContainer,
classes.indicatorsTopContainer,
currentLayout === LAYOUTS.TILE_VIEW && 'tile-view-mode'
) }>
<ThumbnailTopIndicators
currentLayout = { currentLayout }
isFakeScreenShareParticipant = { true }
isHovered = { isHovered }
participantId = { participantId } />
</div>
<div
className = { clsx(classes.indicatorsContainer,
classes.indicatorsBottomContainer,
currentLayout === LAYOUTS.TILE_VIEW && 'tile-view-mode'
) }>
<ThumbnailBottomIndicators
className = { classes.indicatorsBackground }
currentLayout = { currentLayout }
local = { false }
participantId = { participantId }
showStatusIndicators = { false } />
</div>
</span>);
};
export default FakeScreenShareParticipant;

View File

@ -12,7 +12,7 @@ import {
createToolbarEvent,
sendAnalytics
} from '../../../analytics';
import { getToolbarButtons } from '../../../base/config';
import { getSourceNameSignalingFeatureFlag, getToolbarButtons } from '../../../base/config';
import { isMobileBrowser } from '../../../base/environment/utils';
import { translate } from '../../../base/i18n';
import { Icon, IconMenuDown, IconMenuUp } from '../../../base/icons';
@ -107,6 +107,11 @@ type Props = {
*/
_isVerticalFilmstrip: boolean,
/**
* The local screen share participant. This prop is behind the sourceNameSignaling feature flag.
*/
_localScreenShare: Object,
/**
* The maximum width of the vertical filmstrip.
*/
@ -301,6 +306,7 @@ class Filmstrip extends PureComponent <Props, State> {
const {
_currentLayout,
_disableSelfView,
_localScreenShare,
_resizableFilmstrip,
_stageFilmstrip,
_visible,
@ -352,6 +358,20 @@ class Filmstrip extends PureComponent <Props, State> {
}
</div>
)}
{_localScreenShare && !_disableSelfView && !_verticalViewGrid && (
<div
className = 'filmstrip__videos'
id = 'filmstripLocalScreenShare'>
<div id = 'filmstripLocalScreenShareThumbnail'>
{
!tileViewActive && <Thumbnail
key = 'localScreenShare'
participantID = { _localScreenShare.id } />
}
</div>
</div>
)}
{
this._renderRemoteParticipants()
}
@ -782,6 +802,7 @@ function _mapStateToProps(state, ownProps) {
const { testing = {}, iAmRecorder } = state['features/base/config'];
const enableThumbnailReordering = testing.enableThumbnailReordering ?? true;
const { visible, width: verticalFilmstripWidth } = state['features/filmstrip'];
const { localScreenShare } = state['features/base/participants'];
const reduceHeight = state['features/toolbox'].visible && toolbarButtons.length;
const remoteVideosVisible = shouldRemoteVideosBeVisible(state);
const { isOpen: shiftRight } = state['features/chat'];
@ -808,6 +829,7 @@ function _mapStateToProps(state, ownProps) {
_isFilmstripButtonEnabled: isButtonEnabled('filmstrip', state),
_isToolboxVisible: isToolboxVisible(state),
_isVerticalFilmstrip: ownProps._currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW,
_localScreenShare: getSourceNameSignalingFeatureFlag(state) && localScreenShare,
_maxFilmstripWidth: clientWidth - MIN_STAGE_VIEW_WIDTH,
_thumbnailsReordered: enableThumbnailReordering,
_verticalFilmstripWidth: verticalFilmstripWidth.current,

View File

@ -7,6 +7,7 @@ import React, { Component } from 'react';
import { createScreenSharingIssueEvent, sendAnalytics } from '../../../analytics';
import { Avatar } from '../../../base/avatar';
import { getSourceNameSignalingFeatureFlag } from '../../../base/config';
import { isMobileBrowser } from '../../../base/environment/utils';
import { MEDIA_TYPE, VideoTrack } from '../../../base/media';
import {
@ -21,6 +22,7 @@ import {
getLocalAudioTrack,
getLocalVideoTrack,
getTrackByMediaTypeAndParticipant,
getFakeScreenshareParticipantTrack,
updateLastTrackVideoMediaEvent
} from '../../../base/tracks';
import { getVideoObjectPosition } from '../../../facial-recognition/functions';
@ -44,6 +46,7 @@ import {
} from '../../functions';
import { isStageFilmstripEnabled } from '../../functions.web';
import FakeScreenShareParticipant from './FakeScreenShareParticipant';
import ThumbnailAudioIndicator from './ThumbnailAudioIndicator';
import ThumbnailBottomIndicators from './ThumbnailBottomIndicators';
import ThumbnailTopIndicators from './ThumbnailTopIndicators';
@ -132,6 +135,12 @@ export type Props = {|
*/
_isCurrentlyOnLargeVideo: boolean,
/**
* Indicates whether the participant is a fake screen share participant. This prop is behind the
* sourceNameSignaling feature flag.
*/
_isFakeScreenShareParticipant: boolean,
/**
* Whether we are currently running in a mobile browser.
*/
@ -535,6 +544,7 @@ class Thumbnail extends Component<Props, State> {
_currentLayout,
_disableTileEnlargement,
_height,
_isFakeScreenShareParticipant,
_isHidden,
_isScreenSharing,
_participant,
@ -572,7 +582,7 @@ class Thumbnail extends Component<Props, State> {
|| _disableTileEnlargement
|| _isScreenSharing;
if (canPlayEventReceived || _participant.local) {
if (canPlayEventReceived || _participant.local || _isFakeScreenShareParticipant) {
videoStyles = {
objectFit: doNotStretchVideo ? 'contain' : 'cover'
};
@ -1014,7 +1024,7 @@ class Thumbnail extends Component<Props, State> {
* @returns {ReactElement}
*/
render() {
const { _participant } = this.props;
const { _participant, _isFakeScreenShareParticipant } = this.props;
if (!_participant) {
return null;
@ -1030,6 +1040,29 @@ class Thumbnail extends Component<Props, State> {
return this._renderFakeParticipant();
}
if (_isFakeScreenShareParticipant) {
const { isHovered } = this.state;
const { _videoTrack, _isMobile, classes } = this.props;
return (
<FakeScreenShareParticipant
classes = { classes }
containerClassName = { this._getContainerClassName() }
isHovered = { isHovered }
isMobile = { _isMobile }
onClick = { this._onClick }
onMouseEnter = { this._onMouseEnter }
onMouseLeave = { this._onMouseLeave }
onMouseMove = { this._onMouseMove }
onTouchEnd = { this._onTouchEnd }
onTouchMove = { this._onTouchMove }
onTouchStart = { this._onTouchStart }
participantId = { _participant.id }
styles = { this._getStyles() }
videoTrack = { _videoTrack } />
);
}
return this._renderParticipant();
}
}
@ -1049,8 +1082,16 @@ function _mapStateToProps(state, ownProps): Object {
const id = participant?.id;
const isLocal = participant?.local ?? true;
const tracks = state['features/base/tracks'];
const _videoTrack = isLocal
? getLocalVideoTrack(tracks) : getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, participantID);
const sourceNameSignalingEnabled = getSourceNameSignalingFeatureFlag(state);
let _videoTrack;
if (sourceNameSignalingEnabled && participant?.isFakeScreenShareParticipant) {
_videoTrack = getFakeScreenshareParticipantTrack(tracks, id);
} else {
_videoTrack = isLocal
? getLocalVideoTrack(tracks) : getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, participantID);
}
const _audioTrack = isLocal
? getLocalAudioTrack(tracks) : getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.AUDIO, participantID);
const _currentLayout = stageFilmstrip ? LAYOUTS.TILE_VIEW : getCurrentLayout(state);
@ -1144,6 +1185,7 @@ function _mapStateToProps(state, ownProps): Object {
_isAudioOnly: Boolean(state['features/base/audio-only'].enabled),
_isCurrentlyOnLargeVideo: state['features/large-video']?.participantId === id,
_isDominantSpeakerDisabled: interfaceConfig.DISABLE_DOMINANT_SPEAKER_INDICATOR,
_isFakeScreenShareParticipant: sourceNameSignalingEnabled && participant?.isFakeScreenShareParticipant,
_isMobile,
_isMobilePortrait,
_isScreenSharing: _videoTrack?.videoType === 'desktop',

View File

@ -32,7 +32,12 @@ type Props = {
/**
* Id of the participant for which the component is displayed.
*/
participantId: string
participantId: string,
/**
* Whether or not to show the status indicators.
*/
showStatusIndicators: string
}
const useStyles = makeStyles(() => {
@ -58,7 +63,8 @@ const ThumbnailBottomIndicators = ({
className,
currentLayout,
local,
participantId
participantId,
showStatusIndicators = true
}: Props) => {
const styles = useStyles();
const _allowEditing = !useSelector(isNameReadOnly);
@ -66,11 +72,13 @@ const ThumbnailBottomIndicators = ({
const _showDisplayName = useSelector(isDisplayNameVisible);
return (<div className = { className }>
<StatusIndicators
audio = { true }
moderator = { true }
participantID = { participantId }
screenshare = { currentLayout === LAYOUTS.TILE_VIEW } />
{
showStatusIndicators && <StatusIndicators
audio = { true }
moderator = { true }
participantID = { participantId }
screenshare = { currentLayout === LAYOUTS.TILE_VIEW } />
}
{
_showDisplayName && (
<span className = { styles.nameContainer }>

View File

@ -5,6 +5,7 @@ import clsx from 'clsx';
import React from 'react';
import { useSelector } from 'react-redux';
import { getSourceNameSignalingFeatureFlag } from '../../../base/config';
import { isMobileBrowser } from '../../../base/environment/utils';
import ConnectionIndicator from '../../../connection-indicator/components/web/ConnectionIndicator';
import { LAYOUTS } from '../../../video-layout';
@ -40,6 +41,11 @@ type Props = {
*/
isHovered: boolean,
/**
* Whether or not the thumbnail is a fake screen share participant.
*/
isFakeScreenShareParticipant: boolean,
/**
* Whether or not the indicators are for the local participant.
*/
@ -77,6 +83,7 @@ const ThumbnailTopIndicators = ({
currentLayout,
hidePopover,
indicatorsClassName,
isFakeScreenShareParticipant,
isHovered,
local,
participantId,
@ -92,9 +99,24 @@ const ThumbnailTopIndicators = ({
useSelector(state => state['features/base/config'].connectionIndicators?.autoHide) ?? true);
const _connectionIndicatorDisabled = _isMobile
|| Boolean(useSelector(state => state['features/base/config'].connectionIndicators?.disabled));
const sourceNameSignalingEnabled = useSelector(getSourceNameSignalingFeatureFlag);
const showConnectionIndicator = isHovered || !_connectionIndicatorAutoHideEnabled;
if (sourceNameSignalingEnabled && isFakeScreenShareParticipant) {
return (
<div className = { styles.container }>
{!_connectionIndicatorDisabled
&& <ConnectionIndicator
alwaysVisible = { showConnectionIndicator }
enableStatsDisplay = { true }
iconSize = { _indicatorIconSize }
participantId = { participantId }
statsPopoverPosition = { STATS_POPOVER_POSITION[currentLayout] } />
}
</div>
);
}
return (
<>
<div className = { styles.container }>

View File

@ -2,6 +2,7 @@
import React, { Component } from 'react';
import { shouldComponentUpdate } from 'react-window';
import { getSourceNameSignalingFeatureFlag } from '../../../base/config';
import { getLocalParticipant } from '../../../base/participants';
import { connect } from '../../../base/redux';
import { shouldHideSelfView } from '../../../base/settings/functions.any';
@ -30,6 +31,11 @@ type Props = {
*/
_participantID: ?string,
/**
* Whether or not the thumbnail is a local screen share.
*/
_isLocalScreenShare: boolean,
/**
* Whether or not the filmstrip is used a stage filmstrip.
*/
@ -84,6 +90,7 @@ class ThumbnailWrapper extends Component<Props> {
render() {
const {
_disableSelfView,
_isLocalScreenShare = false,
_horizontalOffset = 0,
_participantID,
_stageFilmstrip,
@ -103,6 +110,15 @@ class ThumbnailWrapper extends Component<Props> {
style = { style } />);
}
if (_isLocalScreenShare) {
return _disableSelfView ? null : (
<Thumbnail
horizontalOffset = { _horizontalOffset }
key = 'localScreenShare'
participantID = { _participantID }
style = { style } />);
}
return (
<Thumbnail
horizontalOffset = { _horizontalOffset }
@ -128,6 +144,7 @@ function _mapStateToProps(state, ownProps) {
const { testing = {} } = state['features/base/config'];
const disableSelfView = shouldHideSelfView(state);
const enableThumbnailReordering = testing.enableThumbnailReordering ?? true;
const sourceNameSignalingEnabled = getSourceNameSignalingFeatureFlag(state);
const _verticalViewGrid = showGridInVerticalView(state);
const stageFilmstrip = ownProps.data?.stageFilmstrip;
const remoteParticipants = stageFilmstrip ? activeParticipants : remote;
@ -152,9 +169,22 @@ function _mapStateToProps(state, ownProps) {
const index = (rowIndex * columns) + columnIndex;
let horizontalOffset;
const { iAmRecorder } = state['features/base/config'];
const participantsLength = stageFilmstrip ? remoteParticipantsLength
let participantsLength = stageFilmstrip ? remoteParticipantsLength
: remoteParticipantsLength + (iAmRecorder ? 0 : 1) - (disableSelfView ? 1 : 0);
const { localScreenShare } = state['features/base/participants'];
const localParticipantsLength = localScreenShare ? 2 : 1;
if (sourceNameSignalingEnabled) {
participantsLength = remoteParticipantsLength
// Add local camera and screen share to total participant count when self view is not disabled.
+ (disableSelfView ? 0 : localParticipantsLength)
// Removes iAmRecorder from the total participants count.
- (iAmRecorder ? 1 : 0);
}
if (rowIndex === rows - 1) { // center the last row
const { width: thumbnailWidth } = thumbnailSize;
const partialLastRowParticipantsNumber = participantsLength % columns;
@ -179,7 +209,18 @@ function _mapStateToProps(state, ownProps) {
// When the thumbnails are reordered, local participant is inserted at index 0.
const localIndex = enableThumbnailReordering && !disableSelfView ? 0 : remoteParticipantsLength;
const remoteIndex = enableThumbnailReordering && !iAmRecorder && !disableSelfView ? index - 1 : index;
// Local screen share is inserted at index 1 after the local camera.
const localScreenShareIndex = enableThumbnailReordering && !disableSelfView ? 1 : remoteParticipantsLength;
let remoteIndex;
if (sourceNameSignalingEnabled) {
remoteIndex = enableThumbnailReordering && !iAmRecorder && !disableSelfView
? index - localParticipantsLength : index;
} else {
remoteIndex = enableThumbnailReordering && !iAmRecorder && !disableSelfView ? index - 1 : index;
}
if (!iAmRecorder && index === localIndex) {
return {
@ -189,6 +230,15 @@ function _mapStateToProps(state, ownProps) {
};
}
if (sourceNameSignalingEnabled && !iAmRecorder && localScreenShare && index === localScreenShareIndex) {
return {
_disableSelfView: disableSelfView,
_isLocalScreenShare: true,
_participantID: localScreenShare?.id,
_horizontalOffset: horizontalOffset
};
}
return {
_participantID: remoteParticipants[remoteIndex],
_horizontalOffset: horizontalOffset

View File

@ -1,5 +1,8 @@
// @flow
import { getSourceNameSignalingFeatureFlag } from '../base/config';
import { getFakeScreenShareParticipantOwnerId } from '../base/participants';
import { setRemoteParticipants } from './actions';
import { isReorderingEnabled } from './functions';
@ -15,7 +18,9 @@ export function updateRemoteParticipants(store: Object, participantId: ?number)
const state = store.getState();
let reorderedParticipants = [];
if (!isReorderingEnabled(state)) {
const { sortedRemoteFakeScreenShareParticipants } = state['features/base/participants'];
if (!isReorderingEnabled(state) && !sortedRemoteFakeScreenShareParticipants.size) {
if (participantId) {
const { remoteParticipants } = state['features/filmstrip'];
@ -34,13 +39,28 @@ export function updateRemoteParticipants(store: Object, participantId: ?number)
} = state['features/base/participants'];
const remoteParticipants = new Map(sortedRemoteParticipants);
const screenShares = new Map(sortedRemoteScreenshares);
const screenShareParticipants = sortedRemoteFakeScreenShareParticipants
? [ ...sortedRemoteFakeScreenShareParticipants.keys() ] : [];
const sharedVideos = fakeParticipants ? Array.from(fakeParticipants.keys()) : [];
const speakers = new Map(speakersList);
for (const screenshare of screenShares.keys()) {
remoteParticipants.delete(screenshare);
speakers.delete(screenshare);
if (getSourceNameSignalingFeatureFlag(state)) {
for (const screenshare of screenShareParticipants) {
const ownerId = getFakeScreenShareParticipantOwnerId(screenshare);
remoteParticipants.delete(ownerId);
remoteParticipants.delete(screenshare);
speakers.delete(ownerId);
speakers.delete(screenshare);
}
} else {
for (const screenshare of screenShares.keys()) {
remoteParticipants.delete(screenshare);
speakers.delete(screenshare);
}
}
for (const sharedVideo of sharedVideos) {
remoteParticipants.delete(sharedVideo);
speakers.delete(sharedVideo);
@ -49,13 +69,32 @@ export function updateRemoteParticipants(store: Object, participantId: ?number)
remoteParticipants.delete(speaker);
}
// Always update the order of the thumnails.
reorderedParticipants = [
...Array.from(screenShares.keys()),
...sharedVideos,
...Array.from(speakers.keys()),
...Array.from(remoteParticipants.keys())
];
if (getSourceNameSignalingFeatureFlag(state)) {
// Always update the order of the thumnails.
const participantsWithScreenShare = screenShareParticipants.reduce((acc, screenshare) => {
const ownerId = getFakeScreenShareParticipantOwnerId(screenshare);
acc.push(ownerId);
acc.push(screenshare);
return acc;
}, []);
reorderedParticipants = [
...participantsWithScreenShare,
...sharedVideos,
...Array.from(speakers.keys()),
...Array.from(remoteParticipants.keys())
];
} else {
// Always update the order of the thumnails.
reorderedParticipants = [
...Array.from(screenShares.keys()),
...sharedVideos,
...Array.from(speakers.keys()),
...Array.from(remoteParticipants.keys())
];
}
store.dispatch(setRemoteParticipants(reorderedParticipants));
}

View File

@ -229,9 +229,11 @@ export function getTileDefaultAspectRatio(disableResponsiveTiles, disableTileEnl
export function getNumberOfPartipantsForTileView(state) {
const { iAmRecorder } = state['features/base/config'];
const disableSelfView = shouldHideSelfView(state);
const { localScreenShare } = state['features/base/participants'];
const localParticipantsCount = getSourceNameSignalingFeatureFlag(state) && localScreenShare ? 2 : 1;
const numberOfParticipants = getParticipantCountWithFake(state)
- (iAmRecorder ? 1 : 0)
- (disableSelfView ? 1 : 0);
- (disableSelfView ? localParticipantsCount : 0);
return numberOfParticipants;
}
@ -492,6 +494,7 @@ export function computeDisplayModeFromInput(input: Object) {
isActiveParticipant,
isAudioOnly,
isCurrentlyOnLargeVideo,
isFakeScreenShareParticipant,
isScreenSharing,
canPlayEventReceived,
isRemoteParticipant,
@ -499,6 +502,10 @@ export function computeDisplayModeFromInput(input: Object) {
} = input;
const adjustedIsVideoPlayable = input.isVideoPlayable && (!isRemoteParticipant || canPlayEventReceived);
if (isFakeScreenShareParticipant) {
return DISPLAY_VIDEO;
}
if (!tileViewActive && ((isScreenSharing && isRemoteParticipant) || isActiveParticipant)) {
return DISPLAY_AVATAR;
} else if (isCurrentlyOnLargeVideo && !tileViewActive) {
@ -526,6 +533,7 @@ export function getDisplayModeInput(props: Object, state: Object) {
_isActiveParticipant,
_isAudioOnly,
_isCurrentlyOnLargeVideo,
_isFakeScreenShareParticipant,
_isScreenSharing,
_isVideoPlayable,
_participant,
@ -545,6 +553,7 @@ export function getDisplayModeInput(props: Object, state: Object) {
videoStream: Boolean(_videoTrack),
isRemoteParticipant: !_participant?.isFakeParticipant && !_participant?.local,
isScreenSharing: _isScreenSharing,
isFakeScreenShareParticipant: _isFakeScreenShareParticipant,
videoStreamMuted: _videoTrack ? _videoTrack.muted : 'no stream'
};
}

View File

@ -83,6 +83,10 @@ MiddlewareRegistry.register(store => next => action => {
}
case PARTICIPANT_JOINED: {
result = next(action);
if (action.participant?.isLocalScreenShare) {
break;
}
updateRemoteParticipants(store, action.participant?.id);
break;
}

View File

@ -38,7 +38,8 @@ StateListenerRegistry.register(
/* selector */ state => {
return {
numberOfParticipants: getParticipantCountWithFake(state),
disableSelfView: shouldHideSelfView(state)
disableSelfView: shouldHideSelfView(state),
localScreenShare: state['features/base/participants'].localScreenShare
};
},
/* listener */ (currentState, store) => {

View File

@ -10,6 +10,16 @@
export const SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED
= 'SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED';
/**
* The type of the action which sets the list of known remote fake screen share participant IDs.
*
* @returns {{
* type: FAKE_SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED,
* participantIds: Array<string>
* }}
*/
export const FAKE_SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED = 'FAKE_SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED';
/**
* The type of the action which enables or disables the feature for showing
* video thumbnails in a two-axis tile view.

View File

@ -3,6 +3,7 @@
import type { Dispatch } from 'redux';
import {
FAKE_SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED,
SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED,
SET_TILE_VIEW
} from './actionTypes';
@ -26,6 +27,22 @@ export function setRemoteParticipantsWithScreenShare(participantIds: Array<strin
};
}
/**
* Creates a (redux) action which signals that the list of known remote fake screen share participant ids has changed.
*
* @param {string} participantIds - The remote fake screen share participants.
* @returns {{
* type: FAKE_SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED,
* participantIds: Array<string>
* }}
*/
export function fakeScreenshareParticipantsUpdated(participantIds: Array<string>) {
return {
type: FAKE_SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED,
participantIds
};
}
/**
* Creates a (redux) action which signals to set the UI layout to be tiled view
* or not.

View File

@ -3,6 +3,7 @@
import { ReducerRegistry } from '../base/redux';
import {
FAKE_SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED,
SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED,
SET_TILE_VIEW
} from './actionTypes';
@ -27,6 +28,7 @@ const STORE_NAME = 'features/video-layout';
ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STATE, action) => {
switch (action.type) {
case FAKE_SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED:
case SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED: {
return {
...state,

View File

@ -2,12 +2,45 @@
import debounce from 'lodash/debounce';
import { getSourceNameSignalingFeatureFlag } from '../base/config';
import { StateListenerRegistry, equals } from '../base/redux';
import { isFollowMeActive } from '../follow-me';
import { setRemoteParticipantsWithScreenShare } from './actions';
import { setRemoteParticipantsWithScreenShare, fakeScreenshareParticipantsUpdated } from './actions';
import { getAutoPinSetting, updateAutoPinnedParticipant } from './functions';
StateListenerRegistry.register(
/* selector */ state => state['features/base/participants'].sortedRemoteFakeScreenShareParticipants,
/* listener */ (sortedRemoteFakeScreenShareParticipants, store) => {
if (!getAutoPinSetting() || isFollowMeActive(store) || !getSourceNameSignalingFeatureFlag(store.getState())) {
return;
}
const oldScreenSharesOrder = store.getState()['features/video-layout'].remoteScreenShares || [];
const knownSharingParticipantIds = [ ...sortedRemoteFakeScreenShareParticipants.keys() ];
// Filter out any participants which are no longer screen sharing
// by looping through the known sharing participants and removing any
// participant IDs which are no longer sharing.
const newScreenSharesOrder = oldScreenSharesOrder.filter(
participantId => knownSharingParticipantIds.includes(participantId));
// Make sure all new sharing participant get added to the end of the
// known screen shares.
knownSharingParticipantIds.forEach(participantId => {
if (!newScreenSharesOrder.includes(participantId)) {
newScreenSharesOrder.push(participantId);
}
});
if (!equals(oldScreenSharesOrder, newScreenSharesOrder)) {
store.dispatch(fakeScreenshareParticipantsUpdated(newScreenSharesOrder));
updateAutoPinnedParticipant(oldScreenSharesOrder, store);
}
});
/**
* For auto-pin mode, listen for changes to the known media tracks and look
* for updates to screen shares. The listener is debounced to avoid state
@ -20,7 +53,7 @@ StateListenerRegistry.register(
// possible to have screen sharing participant that has already left in the remoteScreenShares array.
// This can lead to rendering a thumbnails for already left participants since the remoteScreenShares
// array is used for building the ordered list of remote participants.
if (!getAutoPinSetting() || isFollowMeActive(store)) {
if (!getAutoPinSetting() || isFollowMeActive(store) || getSourceNameSignalingFeatureFlag(store.getState())) {
return;
}

View File

@ -250,7 +250,13 @@ function _updateReceiverVideoConstraints({ getState }) {
if (visibleRemoteParticipants?.size) {
visibleRemoteParticipants.forEach(participantId => {
const sourceName = getTrackSourceNameByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, participantId);
let sourceName;
if (remoteScreenShares.includes(participantId)) {
sourceName = participantId;
} else {
sourceName = getTrackSourceNameByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, participantId);
}
if (sourceName) {
visibleRemoteTrackSourceNames.push(sourceName);
@ -262,10 +268,14 @@ function _updateReceiverVideoConstraints({ getState }) {
}
if (localParticipantId !== largeVideoParticipantId) {
largeVideoSourceName = getTrackSourceNameByMediaTypeAndParticipant(
tracks, MEDIA_TYPE.VIDEO,
largeVideoParticipantId
);
if (remoteScreenShares.includes(largeVideoParticipantId)) {
largeVideoSourceName = largeVideoParticipantId;
} else {
largeVideoSourceName = getTrackSourceNameByMediaTypeAndParticipant(
tracks, MEDIA_TYPE.VIDEO,
largeVideoParticipantId
);
}
}
// Tile view.
@ -280,11 +290,7 @@ function _updateReceiverVideoConstraints({ getState }) {
// Prioritize screenshare in tile view.
if (remoteScreenShares?.length) {
const remoteScreenShareSourceNames = remoteScreenShares.map(remoteScreenShare =>
getTrackSourceNameByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, remoteScreenShare)
);
receiverConstraints.selectedSources = remoteScreenShareSourceNames;
receiverConstraints.selectedSources = remoteScreenShares;
}
// Stage view.