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. * The local video identifier.
*/ */
&#filmstripLocalVideo { &#filmstripLocalVideo,
&#filmstripLocalScreenShare {
align-self: flex-end; align-self: flex-end;
display: block; display: block;
margin-bottom: 8px; margin-bottom: 8px;

View File

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

View File

@ -89,7 +89,25 @@
width: 100%; 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. * filmstrip from overlapping the left edge of the screen.
*/ */
#filmstripLocalVideo, #filmstripLocalVideo,
#filmstripLocalScreenShare,
.remote-videos { .remote-videos {
padding: 0; 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 theme from '../../../react/features/base/components/themes/participantsPaneTheme.json';
import { getSourceNameSignalingFeatureFlag } from '../../../react/features/base/config'; import { getSourceNameSignalingFeatureFlag } from '../../../react/features/base/config';
import { i18next } from '../../../react/features/base/i18n'; 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 { import {
getParticipantById, getParticipantById,
getParticipantDisplayName getParticipantDisplayName
} from '../../../react/features/base/participants'; } 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 { CHAT_SIZE } from '../../../react/features/chat';
import { import {
isParticipantConnectionStatusActive, isParticipantConnectionStatusActive,
@ -237,11 +239,14 @@ export default class LargeVideoManager {
let isVideoRenderable; let isVideoRenderable;
if (getSourceNameSignalingFeatureFlag(state)) { if (getSourceNameSignalingFeatureFlag(state)) {
const videoTrack = getTrackByMediaTypeAndParticipant( const tracks = state['features/base/tracks'];
state['features/base/tracks'], MEDIA_TYPE.VIDEO, id); const videoTrack = getVideoTrackByParticipant(tracks, participant);
isVideoRenderable = !isVideoMuted isVideoRenderable = !isVideoMuted && (
&& (APP.conference.isLocalId(id) || isTrackStreamingStatusActive(videoTrack)); APP.conference.isLocalId(id)
|| participant?.isLocalScreenShare
|| isTrackStreamingStatusActive(videoTrack)
);
} else { } else {
isVideoRenderable = !isVideoMuted isVideoRenderable = !isVideoMuted
&& (APP.conference.isLocalId(id) || isParticipantConnectionStatusActive(participant)); && (APP.conference.isLocalId(id) || isParticipantConnectionStatusActive(participant));
@ -268,8 +273,10 @@ export default class LargeVideoManager {
&& participant && !participant.local && !participant.isFakeParticipant) { && participant && !participant.local && !participant.isFakeParticipant) {
// remote participant only // 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'; const isScreenSharing = track?.videoType === 'desktop';
if (isScreenSharing) { if (isScreenSharing) {
@ -300,8 +307,8 @@ export default class LargeVideoManager {
let messageKey; let messageKey;
if (getSourceNameSignalingFeatureFlag(state)) { if (getSourceNameSignalingFeatureFlag(state)) {
const videoTrack = getTrackByMediaTypeAndParticipant( const tracks = state['features/base/tracks'];
state['features/base/tracks'], MEDIA_TYPE.VIDEO, id); const videoTrack = getVideoTrackByParticipant(tracks, participant);
messageKey = isTrackStreamingStatusInactive(videoTrack) ? 'connection.LOW_BANDWIDTH' : null; messageKey = isTrackStreamingStatusInactive(videoTrack) ? 'connection.LOW_BANDWIDTH' : null;
} else { } else {
@ -541,8 +548,8 @@ export default class LargeVideoManager {
const state = APP.store.getState(); const state = APP.store.getState();
if (getSourceNameSignalingFeatureFlag(state)) { if (getSourceNameSignalingFeatureFlag(state)) {
const videoTrack = getTrackByMediaTypeAndParticipant( const tracks = state['features/base/tracks'];
state['features/base/tracks'], MEDIA_TYPE.VIDEO, this.id); const videoTrack = getVideoTrackByParticipant(tracks, participant);
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
show = !APP.conference.isLocalId(this.id) show = !APP.conference.isLocalId(this.id)

View File

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

View File

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

View File

@ -5,6 +5,7 @@ import type { Store } from 'redux';
import { isStageFilmstripEnabled } from '../../filmstrip/functions'; import { isStageFilmstripEnabled } from '../../filmstrip/functions';
import { GRAVATAR_BASE_URL, isCORSAvatarURL } from '../avatar'; import { GRAVATAR_BASE_URL, isCORSAvatarURL } from '../avatar';
import { getSourceNameSignalingFeatureFlag } from '../config';
import { JitsiParticipantConnectionStatus } from '../lib-jitsi-meet'; import { JitsiParticipantConnectionStatus } from '../lib-jitsi-meet';
import { MEDIA_TYPE, shouldRenderVideoTrack } from '../media'; import { MEDIA_TYPE, shouldRenderVideoTrack } from '../media';
import { toState } from '../redux'; import { toState } from '../redux';
@ -119,9 +120,11 @@ export function getNormalizedDisplayName(name: string) {
export function getParticipantById( export function getParticipantById(
stateful: Object | Function, id: string): ?Object { stateful: Object | Function, id: string): ?Object {
const state = toState(stateful)['features/base/participants']; 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} * @returns {number}
*/ */
export function getParticipantCount(stateful: Object | Function) { export function getParticipantCount(stateful: Object | Function) {
const state = toState(stateful)['features/base/participants']; const state = toState(stateful);
const { local, remote, fakeParticipants } = state; 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); 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) { export function getRemoteParticipantCount(stateful: Object | Function) {
const state = toState(stateful)['features/base/participants']; const state = toState(stateful)['features/base/participants'];
if (getSourceNameSignalingFeatureFlag(state)) {
return state.remote.size - state.sortedRemoteFakeScreenShareParticipants.size;
}
return state.remote.size; return state.remote.size;
} }
@ -190,8 +218,12 @@ export function getRemoteParticipantCount(stateful: Object | Function) {
* @returns {number} * @returns {number}
*/ */
export function getParticipantCountWithFake(stateful: Object | Function) { export function getParticipantCountWithFake(stateful: Object | Function) {
const state = toState(stateful)['features/base/participants']; const state = toState(stateful);
const { local, remote } = state; 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); return remote.size + (local ? 1 : 0);
} }

View File

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

View File

@ -107,6 +107,18 @@ export const TRACK_STOPPED = 'TRACK_STOPPED';
*/ */
export const TRACK_UPDATED = 'TRACK_UPDATED'; 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 * 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 * via a WebRTC {@code getUserMedia} call. The action's payload includes an

View File

@ -6,7 +6,7 @@ import {
} from '../../analytics'; } from '../../analytics';
import { NOTIFICATION_TIMEOUT_TYPE, showErrorNotification, showNotification } from '../../notifications'; import { NOTIFICATION_TIMEOUT_TYPE, showErrorNotification, showNotification } from '../../notifications';
import { getCurrentConference } from '../conference'; import { getCurrentConference } from '../conference';
import { getMultipleVideoSupportFeatureFlag } from '../config'; import { getMultipleVideoSupportFeatureFlag, getSourceNameSignalingFeatureFlag } from '../config';
import { JitsiTrackErrors, JitsiTrackEvents, createLocalTrack } from '../lib-jitsi-meet'; import { JitsiTrackErrors, JitsiTrackEvents, createLocalTrack } from '../lib-jitsi-meet';
import { import {
CAMERA_FACING_MODE, CAMERA_FACING_MODE,
@ -21,6 +21,7 @@ import { getLocalParticipant } from '../participants';
import { updateSettings } from '../settings'; import { updateSettings } from '../settings';
import { import {
SCREENSHARE_TRACK_MUTED_UPDATED,
SET_NO_SRC_DATA_NOTIFICATION_UID, SET_NO_SRC_DATA_NOTIFICATION_UID,
TOGGLE_SCREENSHARING, TOGGLE_SCREENSHARING,
TRACK_ADDED, TRACK_ADDED,
@ -395,7 +396,12 @@ export function trackAdded(track) {
return async (dispatch, getState) => { return async (dispatch, getState) => {
track.on( track.on(
JitsiTrackEvents.TRACK_MUTE_CHANGED, JitsiTrackEvents.TRACK_MUTE_CHANGED,
() => dispatch(trackMutedChanged(track))); () => {
if (getSourceNameSignalingFeatureFlag(getState()) && track.getVideoType() === VIDEO_TYPE.DESKTOP) {
dispatch(screenshareTrackMutedChanged(track));
}
dispatch(trackMutedChanged(track));
});
track.on( track.on(
JitsiTrackEvents.TRACK_VIDEOTYPE_CHANGED, JitsiTrackEvents.TRACK_VIDEOTYPE_CHANGED,
type => dispatch(trackVideoTypeChanged(track, type))); 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 * 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. * {@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 { isMobileBrowser } from '../environment/utils';
import JitsiMeetJS, { JitsiTrackErrors, browser } from '../lib-jitsi-meet'; import JitsiMeetJS, { JitsiTrackErrors, browser } from '../lib-jitsi-meet';
import { MEDIA_TYPE, VIDEO_TYPE, setAudioMuted } from '../media'; import { MEDIA_TYPE, VIDEO_TYPE, setAudioMuted } from '../media';
import { getFakeScreenShareParticipantOwnerId } from '../participants';
import { toState } from '../redux'; import { toState } from '../redux';
import { import {
getUserSelectedCameraDeviceId, getUserSelectedCameraDeviceId,
@ -410,6 +411,35 @@ export function getLocalJitsiAudioTrack(state) {
return track?.jitsiTrack; 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. * 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. * 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 { hideNotification, isModerationNotificationDisplayed } from '../../notifications';
import { isPrejoinPageVisible } from '../../prejoin/functions'; import { isPrejoinPageVisible } from '../../prejoin/functions';
import { getCurrentConference } from '../conference/functions'; import { getCurrentConference } from '../conference/functions';
import { getMultipleVideoSupportFeatureFlag } from '../config'; import { getMultipleVideoSupportFeatureFlag, getSourceNameSignalingFeatureFlag } from '../config';
import { getAvailableDevices } from '../devices/actions'; import { getAvailableDevices } from '../devices/actions';
import { import {
CAMERA_FACING_MODE, CAMERA_FACING_MODE,
@ -24,9 +24,11 @@ import {
setScreenshareMuted, setScreenshareMuted,
SCREENSHARE_MUTISM_AUTHORITY SCREENSHARE_MUTISM_AUTHORITY
} from '../media'; } from '../media';
import { participantLeft, participantJoined, getParticipantById } from '../participants';
import { MiddlewareRegistry, StateListenerRegistry } from '../redux'; import { MiddlewareRegistry, StateListenerRegistry } from '../redux';
import { import {
SCREENSHARE_TRACK_MUTED_UPDATED,
TOGGLE_SCREENSHARING, TOGGLE_SCREENSHARING,
TRACK_ADDED, TRACK_ADDED,
TRACK_MUTE_UNMUTE_FAILED, TRACK_MUTE_UNMUTE_FAILED,
@ -50,6 +52,7 @@ import {
isUserInteractionRequiredForUnmute, isUserInteractionRequiredForUnmute,
setTrackMuted setTrackMuted
} from './functions'; } from './functions';
import logger from './logger';
import './subscriber'; import './subscriber';
@ -72,6 +75,13 @@ MiddlewareRegistry.register(store => next => action => {
store.dispatch(getAvailableDevices()); store.dispatch(getAvailableDevices());
} }
if (getSourceNameSignalingFeatureFlag(store.getState())
&& action.track.jitsiTrack.videoType === VIDEO_TYPE.DESKTOP
&& !action.track.jitsiTrack.isMuted()
) {
createFakeScreenShareParticipant(store, action);
}
break; break;
} }
case TRACK_NO_DATA_FROM_SOURCE: { case TRACK_NO_DATA_FROM_SOURCE: {
@ -81,7 +91,40 @@ MiddlewareRegistry.register(store => next => action => {
return result; 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: { 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); _removeNoDataFromSourceNotification(store, action.track);
break; 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 * Gets the local track associated with a specific {@code MEDIA_TYPE} in a
* specific redux store. * specific redux store.

View File

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

View File

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

View File

@ -91,6 +91,11 @@ type Props = {
*/ */
isLocalVideo: boolean, 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 * The send-side max enabled resolution (aka the highest layer that is not
* suspended on the send-side). * suspended on the send-side).
@ -231,9 +236,19 @@ class ConnectionStatsTable extends Component<Props> {
* @returns {ReactElement} * @returns {ReactElement}
*/ */
render() { render() {
const { isLocalVideo, enableSaveLogs, disableShowMoreStats, classes } = this.props; const {
classes,
disableShowMoreStats,
enableSaveLogs,
isFakeScreenShareParticipant,
isLocalVideo
} = this.props;
const className = clsx(classes.connectionStatsTable, { [classes.mobile]: isMobileBrowser() }); const className = clsx(classes.connectionStatsTable, { [classes.mobile]: isMobileBrowser() });
if (isFakeScreenShareParticipant) {
return this._renderScreenShareStatus();
}
return ( return (
<ContextMenu <ContextMenu
className = { classes.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 * Creates a table as ReactElement that will display additional statistics
* related to bandwidth and transport for the local user. * related to bandwidth and transport for the local user.

View File

@ -1,6 +1,7 @@
// @flow // @flow
import type { Dispatch } from 'redux'; import type { Dispatch } from 'redux';
import { getSourceNameSignalingFeatureFlag } from '../base/config';
import { import {
getLocalParticipant, getLocalParticipant,
getParticipantById, getParticipantById,
@ -122,6 +123,7 @@ export function setVerticalViewDimensions() {
const resizableFilmstrip = isFilmstripResizable(state); const resizableFilmstrip = isFilmstripResizable(state);
const _verticalViewGrid = showGridInVerticalView(state); const _verticalViewGrid = showGridInVerticalView(state);
const numberOfRemoteParticipants = getRemoteParticipantCount(state); const numberOfRemoteParticipants = getRemoteParticipantCount(state);
const { localScreenShare } = state['features/base/participants'];
let gridView = {}; let gridView = {};
let thumbnails = {}; let thumbnails = {};
@ -179,6 +181,20 @@ export function setVerticalViewDimensions() {
= thumbnails?.local?.width + TILE_VERTICAL_CONTAINER_HORIZONTAL_MARGIN + SCROLL_SIZE; = thumbnails?.local?.width + TILE_VERTICAL_CONTAINER_HORIZONTAL_MARGIN + SCROLL_SIZE;
remoteVideosContainerHeight remoteVideosContainerHeight
= clientHeight - (disableSelfView ? 0 : thumbnails?.local?.height) - VERTICAL_FILMSTRIP_VERTICAL_MARGIN; = 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 hasScroll
= remoteVideosContainerHeight = remoteVideosContainerHeight
< (thumbnails?.remote.height + TILE_VERTICAL_MARGIN) * numberOfRemoteParticipants; < (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, createToolbarEvent,
sendAnalytics sendAnalytics
} from '../../../analytics'; } from '../../../analytics';
import { getToolbarButtons } from '../../../base/config'; import { getSourceNameSignalingFeatureFlag, getToolbarButtons } from '../../../base/config';
import { isMobileBrowser } from '../../../base/environment/utils'; import { isMobileBrowser } from '../../../base/environment/utils';
import { translate } from '../../../base/i18n'; import { translate } from '../../../base/i18n';
import { Icon, IconMenuDown, IconMenuUp } from '../../../base/icons'; import { Icon, IconMenuDown, IconMenuUp } from '../../../base/icons';
@ -107,6 +107,11 @@ type Props = {
*/ */
_isVerticalFilmstrip: boolean, _isVerticalFilmstrip: boolean,
/**
* The local screen share participant. This prop is behind the sourceNameSignaling feature flag.
*/
_localScreenShare: Object,
/** /**
* The maximum width of the vertical filmstrip. * The maximum width of the vertical filmstrip.
*/ */
@ -301,6 +306,7 @@ class Filmstrip extends PureComponent <Props, State> {
const { const {
_currentLayout, _currentLayout,
_disableSelfView, _disableSelfView,
_localScreenShare,
_resizableFilmstrip, _resizableFilmstrip,
_stageFilmstrip, _stageFilmstrip,
_visible, _visible,
@ -352,6 +358,20 @@ class Filmstrip extends PureComponent <Props, State> {
} }
</div> </div>
)} )}
{_localScreenShare && !_disableSelfView && !_verticalViewGrid && (
<div
className = 'filmstrip__videos'
id = 'filmstripLocalScreenShare'>
<div id = 'filmstripLocalScreenShareThumbnail'>
{
!tileViewActive && <Thumbnail
key = 'localScreenShare'
participantID = { _localScreenShare.id } />
}
</div>
</div>
)}
{ {
this._renderRemoteParticipants() this._renderRemoteParticipants()
} }
@ -782,6 +802,7 @@ function _mapStateToProps(state, ownProps) {
const { testing = {}, iAmRecorder } = state['features/base/config']; const { testing = {}, iAmRecorder } = state['features/base/config'];
const enableThumbnailReordering = testing.enableThumbnailReordering ?? true; const enableThumbnailReordering = testing.enableThumbnailReordering ?? true;
const { visible, width: verticalFilmstripWidth } = state['features/filmstrip']; const { visible, width: verticalFilmstripWidth } = state['features/filmstrip'];
const { localScreenShare } = state['features/base/participants'];
const reduceHeight = state['features/toolbox'].visible && toolbarButtons.length; const reduceHeight = state['features/toolbox'].visible && toolbarButtons.length;
const remoteVideosVisible = shouldRemoteVideosBeVisible(state); const remoteVideosVisible = shouldRemoteVideosBeVisible(state);
const { isOpen: shiftRight } = state['features/chat']; const { isOpen: shiftRight } = state['features/chat'];
@ -808,6 +829,7 @@ function _mapStateToProps(state, ownProps) {
_isFilmstripButtonEnabled: isButtonEnabled('filmstrip', state), _isFilmstripButtonEnabled: isButtonEnabled('filmstrip', state),
_isToolboxVisible: isToolboxVisible(state), _isToolboxVisible: isToolboxVisible(state),
_isVerticalFilmstrip: ownProps._currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW, _isVerticalFilmstrip: ownProps._currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW,
_localScreenShare: getSourceNameSignalingFeatureFlag(state) && localScreenShare,
_maxFilmstripWidth: clientWidth - MIN_STAGE_VIEW_WIDTH, _maxFilmstripWidth: clientWidth - MIN_STAGE_VIEW_WIDTH,
_thumbnailsReordered: enableThumbnailReordering, _thumbnailsReordered: enableThumbnailReordering,
_verticalFilmstripWidth: verticalFilmstripWidth.current, _verticalFilmstripWidth: verticalFilmstripWidth.current,

View File

@ -7,6 +7,7 @@ import React, { Component } from 'react';
import { createScreenSharingIssueEvent, sendAnalytics } from '../../../analytics'; import { createScreenSharingIssueEvent, sendAnalytics } from '../../../analytics';
import { Avatar } from '../../../base/avatar'; import { Avatar } from '../../../base/avatar';
import { getSourceNameSignalingFeatureFlag } from '../../../base/config';
import { isMobileBrowser } from '../../../base/environment/utils'; import { isMobileBrowser } from '../../../base/environment/utils';
import { MEDIA_TYPE, VideoTrack } from '../../../base/media'; import { MEDIA_TYPE, VideoTrack } from '../../../base/media';
import { import {
@ -21,6 +22,7 @@ import {
getLocalAudioTrack, getLocalAudioTrack,
getLocalVideoTrack, getLocalVideoTrack,
getTrackByMediaTypeAndParticipant, getTrackByMediaTypeAndParticipant,
getFakeScreenshareParticipantTrack,
updateLastTrackVideoMediaEvent updateLastTrackVideoMediaEvent
} from '../../../base/tracks'; } from '../../../base/tracks';
import { getVideoObjectPosition } from '../../../facial-recognition/functions'; import { getVideoObjectPosition } from '../../../facial-recognition/functions';
@ -44,6 +46,7 @@ import {
} from '../../functions'; } from '../../functions';
import { isStageFilmstripEnabled } from '../../functions.web'; import { isStageFilmstripEnabled } from '../../functions.web';
import FakeScreenShareParticipant from './FakeScreenShareParticipant';
import ThumbnailAudioIndicator from './ThumbnailAudioIndicator'; import ThumbnailAudioIndicator from './ThumbnailAudioIndicator';
import ThumbnailBottomIndicators from './ThumbnailBottomIndicators'; import ThumbnailBottomIndicators from './ThumbnailBottomIndicators';
import ThumbnailTopIndicators from './ThumbnailTopIndicators'; import ThumbnailTopIndicators from './ThumbnailTopIndicators';
@ -132,6 +135,12 @@ export type Props = {|
*/ */
_isCurrentlyOnLargeVideo: boolean, _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. * Whether we are currently running in a mobile browser.
*/ */
@ -535,6 +544,7 @@ class Thumbnail extends Component<Props, State> {
_currentLayout, _currentLayout,
_disableTileEnlargement, _disableTileEnlargement,
_height, _height,
_isFakeScreenShareParticipant,
_isHidden, _isHidden,
_isScreenSharing, _isScreenSharing,
_participant, _participant,
@ -572,7 +582,7 @@ class Thumbnail extends Component<Props, State> {
|| _disableTileEnlargement || _disableTileEnlargement
|| _isScreenSharing; || _isScreenSharing;
if (canPlayEventReceived || _participant.local) { if (canPlayEventReceived || _participant.local || _isFakeScreenShareParticipant) {
videoStyles = { videoStyles = {
objectFit: doNotStretchVideo ? 'contain' : 'cover' objectFit: doNotStretchVideo ? 'contain' : 'cover'
}; };
@ -1014,7 +1024,7 @@ class Thumbnail extends Component<Props, State> {
* @returns {ReactElement} * @returns {ReactElement}
*/ */
render() { render() {
const { _participant } = this.props; const { _participant, _isFakeScreenShareParticipant } = this.props;
if (!_participant) { if (!_participant) {
return null; return null;
@ -1030,6 +1040,29 @@ class Thumbnail extends Component<Props, State> {
return this._renderFakeParticipant(); 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(); return this._renderParticipant();
} }
} }
@ -1049,8 +1082,16 @@ function _mapStateToProps(state, ownProps): Object {
const id = participant?.id; const id = participant?.id;
const isLocal = participant?.local ?? true; const isLocal = participant?.local ?? true;
const tracks = state['features/base/tracks']; const tracks = state['features/base/tracks'];
const _videoTrack = isLocal 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); ? getLocalVideoTrack(tracks) : getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, participantID);
}
const _audioTrack = isLocal const _audioTrack = isLocal
? getLocalAudioTrack(tracks) : getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.AUDIO, participantID); ? getLocalAudioTrack(tracks) : getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.AUDIO, participantID);
const _currentLayout = stageFilmstrip ? LAYOUTS.TILE_VIEW : getCurrentLayout(state); 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), _isAudioOnly: Boolean(state['features/base/audio-only'].enabled),
_isCurrentlyOnLargeVideo: state['features/large-video']?.participantId === id, _isCurrentlyOnLargeVideo: state['features/large-video']?.participantId === id,
_isDominantSpeakerDisabled: interfaceConfig.DISABLE_DOMINANT_SPEAKER_INDICATOR, _isDominantSpeakerDisabled: interfaceConfig.DISABLE_DOMINANT_SPEAKER_INDICATOR,
_isFakeScreenShareParticipant: sourceNameSignalingEnabled && participant?.isFakeScreenShareParticipant,
_isMobile, _isMobile,
_isMobilePortrait, _isMobilePortrait,
_isScreenSharing: _videoTrack?.videoType === 'desktop', _isScreenSharing: _videoTrack?.videoType === 'desktop',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,6 +10,16 @@
export const SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED export const SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED
= '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 * The type of the action which enables or disables the feature for showing
* video thumbnails in a two-axis tile view. * video thumbnails in a two-axis tile view.

View File

@ -3,6 +3,7 @@
import type { Dispatch } from 'redux'; import type { Dispatch } from 'redux';
import { import {
FAKE_SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED,
SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED, SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED,
SET_TILE_VIEW SET_TILE_VIEW
} from './actionTypes'; } 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 * Creates a (redux) action which signals to set the UI layout to be tiled view
* or not. * or not.

View File

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

View File

@ -2,12 +2,45 @@
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import { getSourceNameSignalingFeatureFlag } from '../base/config';
import { StateListenerRegistry, equals } from '../base/redux'; import { StateListenerRegistry, equals } from '../base/redux';
import { isFollowMeActive } from '../follow-me'; import { isFollowMeActive } from '../follow-me';
import { setRemoteParticipantsWithScreenShare } from './actions'; import { setRemoteParticipantsWithScreenShare, fakeScreenshareParticipantsUpdated } from './actions';
import { getAutoPinSetting, updateAutoPinnedParticipant } from './functions'; 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 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 * 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. // 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 // 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. // array is used for building the ordered list of remote participants.
if (!getAutoPinSetting() || isFollowMeActive(store)) { if (!getAutoPinSetting() || isFollowMeActive(store) || getSourceNameSignalingFeatureFlag(store.getState())) {
return; return;
} }

View File

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