diff --git a/conference.js b/conference.js index 813fbb864..dc71e9e93 100644 --- a/conference.js +++ b/conference.js @@ -103,6 +103,7 @@ import { participantMutedUs, participantPresenceChanged, participantRoleChanged, + participantSourcesUpdated, participantUpdated, screenshareParticipantDisplayNameChanged, updateRemoteParticipantFeatures @@ -1987,6 +1988,11 @@ export default { APP.store.dispatch(participantKicked(kicker, kicked)); }); + room.on(JitsiConferenceEvents.PARTICIPANT_SOURCE_UPDATED, + jitsiParticipant => { + APP.store.dispatch(participantSourcesUpdated(jitsiParticipant)); + }); + room.on(JitsiConferenceEvents.SUSPEND_DETECTED, () => { APP.store.dispatch(suspendDetected()); }); diff --git a/react/features/base/conference/actions.ts b/react/features/base/conference/actions.ts index 38de3cb19..3879a0a18 100644 --- a/react/features/base/conference/actions.ts +++ b/react/features/base/conference/actions.ts @@ -15,9 +15,11 @@ import { participantMutedUs, participantPresenceChanged, participantRoleChanged, + participantSourcesUpdated, participantUpdated } from '../participants/actions'; import { getNormalizedDisplayName } from '../participants/functions'; +import { IJitsiParticipant } from '../participants/types'; import { toState } from '../redux/functions'; import { destroyLocalTracks, @@ -128,6 +130,10 @@ function _addConferenceListeners(conference: IJitsiConference, dispatch: IStore[ JitsiConferenceEvents.PARTICIPANT_KICKED, (kicker: any, kicked: any) => dispatch(participantKicked(kicker, kicked))); + conference.on( + JitsiConferenceEvents.PARTICIPANT_SOURCE_UPDATED, + (jitsiParticipant: IJitsiParticipant) => dispatch(participantSourcesUpdated(jitsiParticipant))); + conference.on( JitsiConferenceEvents.LOCK_STATE_CHANGED, // @ts-ignore (...args: any[]) => dispatch(lockStateChanged(conference, ...args))); diff --git a/react/features/base/conference/functions.ts b/react/features/base/conference/functions.ts index 4fa11f3de..6509043a8 100644 --- a/react/features/base/conference/functions.ts +++ b/react/features/base/conference/functions.ts @@ -105,7 +105,8 @@ export function commonUserJoinedHandling( name: displayName, presence: user.getStatus(), role: user.getRole(), - isReplacing + isReplacing, + sources: user.getSources() })); } } diff --git a/react/features/base/config/configType.ts b/react/features/base/config/configType.ts index 6321fdc8e..b8d19dc4c 100644 --- a/react/features/base/config/configType.ts +++ b/react/features/base/config/configType.ts @@ -336,6 +336,7 @@ export interface IConfig { }; firefox_fake_device?: string; flags?: { + ssrcRewritingEnabled: boolean; }; focusUserJid?: string; gatherStats?: boolean; diff --git a/react/features/base/config/constants.ts b/react/features/base/config/constants.ts index 4983473e3..34bd5f51e 100644 --- a/react/features/base/config/constants.ts +++ b/react/features/base/config/constants.ts @@ -60,3 +60,13 @@ export const PREMEETING_BUTTONS = [ 'microphone', 'camera', 'select-background', * The toolbar buttons to show on 3rdParty prejoin screen. */ export const THIRD_PARTY_PREJOIN_BUTTONS = [ 'microphone', 'camera', 'select-background' ]; + +/** + * The set of feature flags. + * + * @enum {string} + */ + +export const FEATURE_FLAGS = { + SSRC_REWRITING: 'ssrcRewritingEnabled' +}; diff --git a/react/features/base/config/functions.any.ts b/react/features/base/config/functions.any.ts index 29eda918e..504b5859f 100644 --- a/react/features/base/config/functions.any.ts +++ b/react/features/base/config/functions.any.ts @@ -11,7 +11,7 @@ import { parseURLParams } from '../util/parseURLParams'; import { IConfig } from './configType'; import CONFIG_WHITELIST from './configWhitelist'; -import { _CONFIG_STORE_PREFIX } from './constants'; +import { FEATURE_FLAGS, _CONFIG_STORE_PREFIX } from './constants'; import INTERFACE_CONFIG_WHITELIST from './interfaceConfigWhitelist'; import logger from './logger'; @@ -63,6 +63,16 @@ export function getMultipleVideoSendingSupportFeatureFlag(state: IReduxState) { return isUnifiedPlanEnabled(state); } +/** + * Selector used to get the SSRC-rewriting feature flag. + * + * @param {Object} state - The global state. + * @returns {boolean} + */ +export function getSsrcRewritingFeatureFlag(state: IReduxState) { + return getFeatureFlag(state, FEATURE_FLAGS.SSRC_REWRITING); +} + /** * Selector used to get a feature flag. * diff --git a/react/features/base/participants/actionTypes.ts b/react/features/base/participants/actionTypes.ts index c6dec5571..4e13afbc5 100644 --- a/react/features/base/participants/actionTypes.ts +++ b/react/features/base/participants/actionTypes.ts @@ -117,6 +117,18 @@ export const PARTICIPANT_KICKED = 'PARTICIPANT_KICKED'; */ export const PARTICIPANT_LEFT = 'PARTICIPANT_LEFT'; +/** + * Action to handle case when the sources attached to a participant are updated. + * + * { + * type: PARTICIPANT_SOURCES_UPDATED, + * participant: { + * id: string + * } + * } + */ +export const PARTICIPANT_SOURCES_UPDATED = 'PARTICIPANT_SOURCES_UPDATED'; + /** * Action to handle case when info about participant changes. * diff --git a/react/features/base/participants/actions.ts b/react/features/base/participants/actions.ts index 2f4dc3005..4c336ab33 100644 --- a/react/features/base/participants/actions.ts +++ b/react/features/base/participants/actions.ts @@ -18,6 +18,7 @@ import { PARTICIPANT_JOINED, PARTICIPANT_KICKED, PARTICIPANT_LEFT, + PARTICIPANT_SOURCES_UPDATED, PARTICIPANT_UPDATED, PIN_PARTICIPANT, RAISE_HAND_UPDATED, @@ -36,7 +37,7 @@ import { getVirtualScreenshareParticipantOwnerId } from './functions'; import logger from './logger'; -import { FakeParticipant, IParticipant } from './types'; +import { FakeParticipant, IJitsiParticipant, IParticipant } from './types'; /** * Create an action for when dominant speaker changes. @@ -253,6 +254,39 @@ export function participantJoined(participant: IParticipant) { }; } +/** + * Updates the sources of a remote participant. + * + * @param {IJitsiParticipant} jitsiParticipant - The IJitsiParticipant instance. + * @returns {{ + * type: PARTICIPANT_SOURCES_UPDATED, + * participant: IParticipant + * }} + */ +export function participantSourcesUpdated(jitsiParticipant: IJitsiParticipant) { + return (dispatch: IStore['dispatch'], getState: IStore['getState']) => { + const id = jitsiParticipant.getId(); + const participant = getParticipantById(getState(), id); + + if (participant?.local) { + return; + } + const sources = jitsiParticipant.getSources(); + + if (!sources?.size) { + return; + } + + return dispatch({ + type: PARTICIPANT_SOURCES_UPDATED, + participant: { + id, + sources + } + }); + }; +} + /** * Updates the features of a remote participant. * diff --git a/react/features/base/participants/functions.ts b/react/features/base/participants/functions.ts index 45465ee04..4045711ef 100644 --- a/react/features/base/participants/functions.ts +++ b/react/features/base/participants/functions.ts @@ -6,8 +6,9 @@ import { isStageFilmstripAvailable } from '../../filmstrip/functions'; import { IStateful } from '../app/types'; import { GRAVATAR_BASE_URL } from '../avatar/constants'; import { isCORSAvatarURL } from '../avatar/functions'; +import { getCurrentConference } from '../conference/functions'; import i18next from '../i18n/i18next'; -import { VIDEO_TYPE } from '../media/constants'; +import { MEDIA_TYPE, VIDEO_TYPE } from '../media/constants'; import { toState } from '../redux/functions'; import { getScreenShareTrack } from '../tracks/functions'; import { createDeferred } from '../util/helpers'; @@ -21,7 +22,7 @@ import { // eslint-disable-next-line lines-around-comment // @ts-ignore import { preloadImage } from './preloadImage'; -import { FakeParticipant, IParticipant } from './types'; +import { FakeParticipant, IJitsiParticipant, IParticipant, ISourceInfo } from './types'; /** * Temp structures for avatar urls to be checked/preloaded. @@ -407,6 +408,35 @@ export function getParticipantDisplayName(stateful: IStateful, id: string): stri return defaultRemoteDisplayName ?? ''; } +/** + * Returns the source names of the screenshare sources in the conference based on the presence shared by the remote + * endpoints. This should be only used for creating/removing virtual screenshare participant tiles when ssrc-rewriting + * is enabled. Once the tile is created, the source-name gets added to the receiver constraints based on which the + * JVB will add the source to the video sources map and signal it to the local endpoint. Only then, a remote track is + * created/remapped and the tracks in redux will be updated. Once the track is updated in redux, the client will + * will continue to use the other track based getter functions for other operations related to screenshare. + * + * @param {(Function|Object)} stateful - The (whole) redux state, or redux's {@code getState} function to be used to + * retrieve the state. + * @returns {string[]} + */ +export function getRemoteScreensharesBasedOnPresence(stateful: IStateful): string[] { + const conference = getCurrentConference(stateful); + + return conference?.getParticipants()?.reduce((screenshares: string[], participant: IJitsiParticipant) => { + const sources: Map> = participant.getSources(); + const videoSources = sources.get(MEDIA_TYPE.VIDEO); + const screenshareSources = Array.from(videoSources ?? new Map()) + .filter(source => source[1].videoType === VIDEO_TYPE.DESKTOP && !source[1].muted) + .map(source => source[0]); + + // eslint-disable-next-line no-param-reassign + screenshares = [ ...screenshares, ...screenshareSources ]; + + return screenshares; + }, []); +} + /** * Returns screenshare participant's display name. * @@ -434,6 +464,36 @@ export function getScreenshareParticipantIds(stateful: IStateful): Array .map(t => t.participantId); } +/** + * Returns a list source name associated with a given remote participant and for the given media type. + * + * @param {(Function|Object)} stateful - The (whole) redux state, or redux's {@code getState} function to be used to + * retrieve the state. + * @param {string} id - The id of the participant whose source names are to be retrieved. + * @param {string} mediaType - The type of source, audio or video. + * @returns {Array|undefined} + */ +export function getSourceNamesByMediaType( + stateful: IStateful, + id: string, + mediaType: string): Array | undefined { + const participant: IParticipant | undefined = getParticipantById(stateful, id); + + if (!participant) { + return; + } + + const sources = participant.sources; + + if (!sources) { + return; + } + + return Array.from(sources.get(mediaType) ?? new Map()) + .filter(source => source[1].videoType !== VIDEO_TYPE.DESKTOP || !source[1].muted) + .map(s => s[0]); +} + /** * Returns the presence status of a participant associated with the passed id. * diff --git a/react/features/base/participants/reducer.ts b/react/features/base/participants/reducer.ts index 17584571c..1b1ccc399 100644 --- a/react/features/base/participants/reducer.ts +++ b/react/features/base/participants/reducer.ts @@ -1,3 +1,4 @@ +import { MEDIA_TYPE } from '../media/constants'; import ReducerRegistry from '../redux/ReducerRegistry'; import { set } from '../redux/functions'; @@ -7,6 +8,7 @@ import { PARTICIPANT_ID_CHANGED, PARTICIPANT_JOINED, PARTICIPANT_LEFT, + PARTICIPANT_SOURCES_UPDATED, PARTICIPANT_UPDATED, PIN_PARTICIPANT, RAISE_HAND_UPDATED, @@ -20,7 +22,7 @@ import { isRemoteScreenshareParticipant, isScreenShareParticipant } from './functions'; -import { ILocalParticipant, IParticipant } from './types'; +import { FakeParticipant, ILocalParticipant, IParticipant, ISourceInfo } from './types'; /** * Participant object. @@ -69,6 +71,7 @@ const DEFAULT_STATE = { pinnedParticipant: undefined, raisedHandsQueue: [], remote: new Map(), + remoteVideoSources: new Set(), sortedRemoteVirtualScreenshareParticipants: new Map(), sortedRemoteParticipants: new Map(), speakersList: new Map() @@ -84,6 +87,7 @@ export interface IParticipantsState { pinnedParticipant?: string; raisedHandsQueue: Array<{ id: string; raisedHandTimestamp: number; }>; remote: Map; + remoteVideoSources: Set; sortedRemoteParticipants: Map; sortedRemoteVirtualScreenshareParticipants: Map; speakersList: Map; @@ -243,7 +247,8 @@ ReducerRegistry.register('features/base/participants', fakeParticipant, id, name, - pinned + pinned, + sources } = participant; const { pinnedParticipant, dominantSpeaker } = state; @@ -287,6 +292,19 @@ ReducerRegistry.register('features/base/participants', state.remote.set(id, participant); + if (sources?.size) { + const videoSources: Map | undefined = sources.get(MEDIA_TYPE.VIDEO); + + if (videoSources?.size) { + const newRemoteVideoSources = new Set(state.remoteVideoSources); + + for (const source of videoSources.keys()) { + newRemoteVideoSources.add(source); + } + state.remoteVideoSources = newRemoteVideoSources; + } + } + // Insert the new participant. const displayName = _getDisplayName(state, name); const sortedRemoteParticipants = Array.from(state.sortedRemoteParticipants); @@ -332,6 +350,23 @@ ReducerRegistry.register('features/base/participants', } = state; let oldParticipant = remote.get(id); + if (oldParticipant?.sources?.size) { + const videoSources: Map | undefined = oldParticipant.sources.get(MEDIA_TYPE.VIDEO); + const newRemoteVideoSources = new Set(state.remoteVideoSources); + + if (videoSources?.size) { + for (const source of videoSources.keys()) { + newRemoteVideoSources.delete(source); + } + } + state.remoteVideoSources = newRemoteVideoSources; + } else if (oldParticipant?.fakeParticipant === FakeParticipant.RemoteScreenShare) { + const newRemoteVideoSources = new Set(state.remoteVideoSources); + + newRemoteVideoSources.delete(id); + state.remoteVideoSources = newRemoteVideoSources; + } + if (oldParticipant && oldParticipant.conference === conference) { remote.delete(id); } else if (local?.id === id) { @@ -374,6 +409,26 @@ ReducerRegistry.register('features/base/participants', return { ...state }; } + case PARTICIPANT_SOURCES_UPDATED: { + const { id, sources } = action.participant; + const participant = state.remote.get(id); + + if (participant) { + participant.sources = sources; + const videoSources: Map = sources.get(MEDIA_TYPE.VIDEO); + + if (videoSources?.size) { + const newRemoteVideoSources = new Set(state.remoteVideoSources); + + for (const source of videoSources.keys()) { + newRemoteVideoSources.add(source); + } + state.remoteVideoSources = newRemoteVideoSources; + } + } + + return { ...state }; + } case RAISE_HAND_UPDATED: { return { ...state, @@ -493,7 +548,8 @@ function _participantJoined({ participant }: { participant: IParticipant; }) { name, pinned, presence, - role + role, + sources } = participant; let { conference, id } = participant; @@ -523,7 +579,8 @@ function _participantJoined({ participant }: { participant: IParticipant; }) { name, pinned: pinned || false, presence, - role: role || PARTICIPANT_ROLE.NONE + role: role || PARTICIPANT_ROLE.NONE, + sources }; } diff --git a/react/features/base/participants/subscriber.ts b/react/features/base/participants/subscriber.ts index 26396f9d7..82a76c37d 100644 --- a/react/features/base/participants/subscriber.ts +++ b/react/features/base/participants/subscriber.ts @@ -3,11 +3,14 @@ import _ from 'lodash'; import { IStore } from '../../app/types'; import { getCurrentConference } from '../conference/functions'; import { - getMultipleVideoSendingSupportFeatureFlag + getMultipleVideoSendingSupportFeatureFlag, + getSsrcRewritingFeatureFlag } from '../config/functions.any'; +import { VIDEO_TYPE } from '../media/constants'; import StateListenerRegistry from '../redux/StateListenerRegistry'; import { createVirtualScreenshareParticipant, participantLeft } from './actions'; +import { getRemoteScreensharesBasedOnPresence } from './functions'; import { FakeParticipant } from './types'; StateListenerRegistry.register( @@ -15,13 +18,50 @@ StateListenerRegistry.register( /* listener */(tracks, store) => _updateScreenshareParticipants(store) ); +StateListenerRegistry.register( + /* selector */ state => state['features/base/participants'].remoteVideoSources, + /* listener */(remoteVideoSources, store) => getSsrcRewritingFeatureFlag(store.getState()) + && _updateScreenshareParticipantsBasedOnPresence(store) +); + +/** + * Compares the old and new screenshare lists provided and creates/removes the virtual screenshare participant + * tiles accodingly. + * + * @param {Array} oldScreenshareSourceNames - List of old screenshare source names. + * @param {Array} newScreenshareSourceNames - Current list of screenshare source names. + * @param {Object} store - The redux store. + * @returns {void} + */ +function _createOrRemoveVirtualParticipants( + oldScreenshareSourceNames: string[], + newScreenshareSourceNames: string[], + store: IStore): void { + const { dispatch, getState } = store; + const conference = getCurrentConference(getState()); + const removedScreenshareSourceNames = _.difference(oldScreenshareSourceNames, newScreenshareSourceNames); + const addedScreenshareSourceNames = _.difference(newScreenshareSourceNames, oldScreenshareSourceNames); + + if (removedScreenshareSourceNames.length) { + removedScreenshareSourceNames.forEach(id => dispatch(participantLeft(id, conference, { + fakeParticipant: FakeParticipant.RemoteScreenShare + }))); + } + + if (addedScreenshareSourceNames.length) { + addedScreenshareSourceNames.forEach(id => dispatch( + createVirtualScreenshareParticipant(id, false, conference))); + } +} + /** * Handles creating and removing virtual screenshare participants. * * @param {*} store - The redux store. * @returns {void} */ -function _updateScreenshareParticipants({ getState, dispatch }: IStore) { +function _updateScreenshareParticipants(store: IStore): void { + const { dispatch, getState } = store; const state = getState(); const conference = getCurrentConference(state); const tracks = state['features/base/tracks']; @@ -31,7 +71,7 @@ function _updateScreenshareParticipants({ getState, dispatch }: IStore) { let newLocalSceenshareSourceName; const currentScreenshareSourceNames = tracks.reduce((acc: string[], track) => { - if (track.videoType === 'desktop' && !track.jitsiTrack.isMuted()) { + if (track.videoType === VIDEO_TYPE.DESKTOP && !track.jitsiTrack.isMuted()) { const sourceName: string = track.jitsiTrack.getSourceName(); if (track.local) { @@ -51,24 +91,30 @@ function _updateScreenshareParticipants({ getState, dispatch }: IStore) { if (localScreenShare && !newLocalSceenshareSourceName) { dispatch(participantLeft(localScreenShare.id, conference, { - fakeParticipant: FakeParticipant.LocalScreenShare, - isReplaced: undefined + fakeParticipant: FakeParticipant.LocalScreenShare })); } } - const removedScreenshareSourceNames = _.difference(previousScreenshareSourceNames, currentScreenshareSourceNames); - const addedScreenshareSourceNames = _.difference(currentScreenshareSourceNames, previousScreenshareSourceNames); - - if (removedScreenshareSourceNames.length) { - removedScreenshareSourceNames.forEach(id => dispatch(participantLeft(id, conference, { - fakeParticipant: FakeParticipant.RemoteScreenShare, - isReplaced: undefined - }))); + if (getSsrcRewritingFeatureFlag(state)) { + return; } - if (addedScreenshareSourceNames.length) { - addedScreenshareSourceNames.forEach(id => dispatch( - createVirtualScreenshareParticipant(id, false, conference))); - } + _createOrRemoveVirtualParticipants(previousScreenshareSourceNames, currentScreenshareSourceNames, store); +} + +/** + * Handles the creation and removal of remote virtual screenshare participants when ssrc-rewriting is enabled. + * + * @param {Object} store - The redux store. + * @returns {void} + */ +function _updateScreenshareParticipantsBasedOnPresence(store: IStore): void { + const { getState } = store; + const state = getState(); + const { sortedRemoteVirtualScreenshareParticipants } = state['features/base/participants']; + const previousScreenshareSourceNames = [ ...sortedRemoteVirtualScreenshareParticipants.keys() ]; + const currentScreenshareSourceNames = getRemoteScreensharesBasedOnPresence(state); + + _createOrRemoveVirtualParticipants(previousScreenshareSourceNames, currentScreenshareSourceNames, store); } diff --git a/react/features/base/participants/types.ts b/react/features/base/participants/types.ts index 58cdd4b40..98e8a0403 100644 --- a/react/features/base/participants/types.ts +++ b/react/features/base/participants/types.ts @@ -37,6 +37,7 @@ export interface IParticipant { region?: string; remoteControlSessionStatus?: boolean; role?: string; + sources?: Map>; supportsRemoteControl?: boolean; } @@ -51,10 +52,16 @@ export interface ILocalParticipant extends IParticipant { userSelectedMicDeviceLabel?: string; } +export interface ISourceInfo { + muted: boolean; + videoType: string; +} + export interface IJitsiParticipant { getDisplayName: () => string; getId: () => string; getJid: () => string; getRole: () => string; + getSources: () => Map>; isHidden: () => boolean; } diff --git a/react/features/base/tracks/actionTypes.ts b/react/features/base/tracks/actionTypes.ts index 151880c0d..705c1a583 100644 --- a/react/features/base/tracks/actionTypes.ts +++ b/react/features/base/tracks/actionTypes.ts @@ -66,6 +66,16 @@ export const TRACK_MUTE_UNMUTE_FAILED = 'TRACK_MUTE_UNMUTE_FAILED'; */ export const TRACK_NO_DATA_FROM_SOURCE = 'TRACK_NO_DATA_FROM_SOURCE'; +/** + * The type of redux action dispatched when the owner of a track changes due to ssrc remapping. + * + * { + * type: TRACK_OWNER_CHANGED, + * track: Track + * } + */ +export const TRACK_OWNER_CHANGED = 'TRACK_OWNER_CHANGED'; + /** * The type of redux action dispatched when a track has been (locally or * remotely) removed from the conference. @@ -96,7 +106,7 @@ export const TRACK_STOPPED = 'TRACK_STOPPED'; * } */ export const TRACK_UPDATED = 'TRACK_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 diff --git a/react/features/base/tracks/actions.any.ts b/react/features/base/tracks/actions.any.ts index e6b538bf5..6901e9936 100644 --- a/react/features/base/tracks/actions.any.ts +++ b/react/features/base/tracks/actions.any.ts @@ -27,6 +27,7 @@ import { TRACK_CREATE_ERROR, TRACK_MUTE_UNMUTE_FAILED, TRACK_NO_DATA_FROM_SOURCE, + TRACK_OWNER_CHANGED, TRACK_REMOVED, TRACK_STOPPED, TRACK_UPDATED, @@ -377,7 +378,9 @@ export function trackAdded(track: any) { track.on( JitsiTrackEvents.TRACK_VIDEOTYPE_CHANGED, (type: VideoType) => dispatch(trackVideoTypeChanged(track, type))); - + track.on( + JitsiTrackEvents.TRACK_OWNER_CHANGED, + (owner: string) => dispatch(trackOwnerChanged(track, owner))); const local = track.isLocal(); const isVirtualScreenshareParticipantCreated = !local || getMultipleVideoSendingSupportFeatureFlag(getState()); const mediaType = track.getVideoType() === VIDEO_TYPE.DESKTOP && isVirtualScreenshareParticipantCreated @@ -582,11 +585,14 @@ export function trackVideoStarted(track: any): { * }} */ export function trackVideoTypeChanged(track: any, videoType: VideoType) { + const mediaType = videoType === VIDEO_TYPE.CAMERA ? MEDIA_TYPE.VIDEO : MEDIA_TYPE.SCREENSHARE; + return { type: TRACK_UPDATED, track: { jitsiTrack: track, - videoType + videoType, + mediaType } }; } @@ -617,6 +623,32 @@ export function trackStreamingStatusChanged(track: any, streamingStatus: string) }; } +/** + * Create an action for when the owner of the track changes due to ssrc remapping. + * + * @param {(JitsiRemoteTrack)} track - JitsiTrack instance. + * @param {string} participantId - New owner's participant ID. + * @returns {{ + * type: TRACK_OWNER_CHANGED, + * track: Track + * }} + */ +export function trackOwnerChanged(track: any, participantId: string): { + track: { + jitsiTrack: any; + participantId: string; + }; + type: 'TRACK_OWNER_CHANGED'; +} { + return { + type: TRACK_OWNER_CHANGED, + track: { + jitsiTrack: track, + participantId + } + }; +} + /** * Signals passed tracks to be added. * diff --git a/react/features/base/tracks/reducer.ts b/react/features/base/tracks/reducer.ts index 26f0fac50..7f12ec959 100644 --- a/react/features/base/tracks/reducer.ts +++ b/react/features/base/tracks/reducer.ts @@ -8,6 +8,7 @@ import { TRACK_CREATE_CANCELED, TRACK_CREATE_ERROR, TRACK_NO_DATA_FROM_SOURCE, + TRACK_OWNER_CHANGED, TRACK_REMOVED, TRACK_UPDATED, TRACK_UPDATE_LAST_VIDEO_MEDIA_EVENT, @@ -41,6 +42,18 @@ function track(state: ITrack, action: any) { } break; + case TRACK_OWNER_CHANGED: { + const t = action.track; + + if (state.jitsiTrack === t.jitsiTrack) { + return { + ...state, + participantId: t.participantId + }; + } + break; + } + case TRACK_UPDATED: { const t = action.track; @@ -103,10 +116,10 @@ ReducerRegistry.register('features/base/tracks', (state = [], acti switch (action.type) { case PARTICIPANT_ID_CHANGED: case TRACK_NO_DATA_FROM_SOURCE: + case TRACK_OWNER_CHANGED: case TRACK_UPDATE_LAST_VIDEO_MEDIA_EVENT: case TRACK_UPDATED: return state.map((t: ITrack) => track(t, action)); - case TRACK_ADDED: { let withoutTrackStub = state; diff --git a/react/features/large-video/actions.any.ts b/react/features/large-video/actions.any.ts index 4bf2c3eba..2a7076df8 100644 --- a/react/features/large-video/actions.any.ts +++ b/react/features/large-video/actions.any.ts @@ -1,4 +1,5 @@ import { IReduxState, IStore } from '../app/types'; +import { getSsrcRewritingFeatureFlag } from '../base/config/functions.any'; import { MEDIA_TYPE } from '../base/media/constants'; import { getDominantSpeakerParticipant, @@ -170,11 +171,14 @@ function _electParticipantInLargeVideo(state: IReduxState) { participant = undefined; // Next, pick the most recent participant with video. - const tracks = state['features/base/tracks']; - const videoTrack = _electLastVisibleRemoteVideo(tracks); + // (Skip this if rewriting, tracks may be detached from any owner.) + if (!getSsrcRewritingFeatureFlag(state)) { + const tracks = state['features/base/tracks']; + const videoTrack = _electLastVisibleRemoteVideo(tracks); - if (videoTrack) { - return videoTrack.participantId; + if (videoTrack) { + return videoTrack.participantId; + } } // Last, select the participant that joined last (other than poltergist or other bot type participants). diff --git a/react/features/video-quality/subscriber.ts b/react/features/video-quality/subscriber.ts index 11a74da87..c77f30709 100644 --- a/react/features/video-quality/subscriber.ts +++ b/react/features/video-quality/subscriber.ts @@ -1,9 +1,13 @@ import debounce from 'lodash/debounce'; -import { IStore } from '../app/types'; +import { IReduxState, IStore } from '../app/types'; import { _handleParticipantError } from '../base/conference/functions'; +import { getSsrcRewritingFeatureFlag } from '../base/config/functions.any'; import { MEDIA_TYPE } from '../base/media/constants'; -import { getLocalParticipant } from '../base/participants/functions'; +import { + getLocalParticipant, + getSourceNamesByMediaType +} from '../base/participants/functions'; import StateListenerRegistry from '../base/redux/StateListenerRegistry'; import { getTrackSourceNameByMediaTypeAndParticipant } from '../base/tracks/functions'; import { reportError } from '../base/util/helpers'; @@ -94,6 +98,15 @@ StateListenerRegistry.register( } ); +/** + * Updates the receiver constraints when new video sources are added to the conference. + */ +StateListenerRegistry.register( + /* selector */ state => state['features/base/participants'].remoteVideoSources, + /* listener */ (remoteVideoSources, store) => { + getSsrcRewritingFeatureFlag(store.getState()) && _updateReceiverVideoConstraints(store); + }); + /** * StateListenerRegistry provides a reliable way of detecting changes to * maxReceiverVideoQuality* and preferredVideoQuality state and dispatching additional actions. @@ -294,6 +307,42 @@ StateListenerRegistry.register( deepEquals: true }); +/** + * Returns the source names asociated with the given participants list. + * + * @param {Array} participantList - The list of participants. + * @param {Object} state - The redux state. + * @returns {Array} + */ +function _getSourceNames(participantList: Array, state: IReduxState): Array { + const { remoteScreenShares } = state['features/video-layout']; + const tracks = state['features/base/tracks']; + const sourceNamesList: string[] = []; + + participantList.forEach(participantId => { + if (getSsrcRewritingFeatureFlag(state)) { + const sourceNames: string[] | undefined + = getSourceNamesByMediaType(state, participantId, MEDIA_TYPE.VIDEO); + + sourceNames?.length && sourceNamesList.push(...sourceNames); + } else { + let sourceName: string; + + if (remoteScreenShares.includes(participantId)) { + sourceName = participantId; + } else { + sourceName = getTrackSourceNameByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, participantId); + } + + if (sourceName) { + sourceNamesList.push(sourceName); + } + } + }); + + return sourceNamesList; +} + /** * Helper function for updating the preferred sender video constraint, based on the user preference. * @@ -360,53 +409,29 @@ function _updateReceiverVideoConstraints({ getState }: IStore) { lastN }; - const activeParticipantsSources: string[] = []; - const visibleRemoteTrackSourceNames: string[] = []; + let activeParticipantsSources: string[] = []; + let visibleRemoteTrackSourceNames: string[] = []; let largeVideoSourceName: string | undefined; receiverConstraints.onStageSources = []; receiverConstraints.selectedSources = []; if (visibleRemoteParticipants?.size) { - visibleRemoteParticipants.forEach(participantId => { - let sourceName; - - if (remoteScreenShares.includes(participantId)) { - sourceName = participantId; - } else { - sourceName = getTrackSourceNameByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, participantId); - } - - if (sourceName) { - visibleRemoteTrackSourceNames.push(sourceName); - } - }); + visibleRemoteTrackSourceNames = _getSourceNames(Array.from(visibleRemoteParticipants), state); } if (activeParticipantsIds?.length > 0) { - activeParticipantsIds.forEach((participantId: string) => { - let sourceName; - - if (remoteScreenShares.includes(participantId)) { - sourceName = participantId; - } else { - sourceName = getTrackSourceNameByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, participantId); - } - - if (sourceName) { - activeParticipantsSources.push(sourceName); - } - }); - + activeParticipantsSources = _getSourceNames(activeParticipantsIds, state); } if (localParticipantId !== largeVideoParticipantId) { if (remoteScreenShares.includes(largeVideoParticipantId)) { largeVideoSourceName = largeVideoParticipantId; } else { - largeVideoSourceName = getTrackSourceNameByMediaTypeAndParticipant( - tracks, MEDIA_TYPE.VIDEO, largeVideoParticipantId - ); + largeVideoSourceName = getSsrcRewritingFeatureFlag(state) + ? getSourceNamesByMediaType(state, largeVideoParticipantId, MEDIA_TYPE.VIDEO)?.[0] + : getTrackSourceNameByMediaTypeAndParticipant( + tracks, MEDIA_TYPE.VIDEO, largeVideoParticipantId); } }