Feat ssrc rewriting (#12408)

Use a fixed set of remote tracks for a call based on the ssrc-limit set in jvb config. When this feature is enabled, JVB will signal the audio and video SSRCs and their owner/endpoint info using a bridge channel message. For audio, the mappings are determined based on the energy rankings on the audio sources present in the call and for video, the mappings are calculated based on the video sources the client requests through the video receiver constraints.
Remote tracks are then created/remapped by the client based on these mappings.

* Added track_owner_changed action
* Skip track-based large-video selection in rewriting mode.
* Register OWNER_CHANGED handler at track level.
* feat(participants) Add source info to participants in redux.
With ssrc-rewriting, the receiver constraints need to be generated using the source info received in presence. Currently they are generated from the track info in redux but with ssrc-rewriting, remote sources are not signaled and therefore created until they are being requested through receiver constraints.

Co-authored-by: James A <jqdrqgnq@users.noreply.github.com>
This commit is contained in:
Jaya Allamsetty 2023-01-24 13:58:58 -05:00 committed by GitHub
parent cbae997eda
commit 5e90e72562
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 402 additions and 68 deletions

View File

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

View File

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

View File

@ -105,7 +105,8 @@ export function commonUserJoinedHandling(
name: displayName,
presence: user.getStatus(),
role: user.getRole(),
isReplacing
isReplacing,
sources: user.getSources()
}));
}
}

View File

@ -336,6 +336,7 @@ export interface IConfig {
};
firefox_fake_device?: string;
flags?: {
ssrcRewritingEnabled: boolean;
};
focusUserJid?: string;
gatherStats?: boolean;

View File

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

View File

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

View File

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

View File

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

View File

@ -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<string, Map<string, ISourceInfo>> = 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<string>
.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<string>|undefined}
*/
export function getSourceNamesByMediaType(
stateful: IStateful,
id: string,
mediaType: string): Array<string> | 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.
*

View File

@ -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<string>(),
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<string, IParticipant>;
remoteVideoSources: Set<string>;
sortedRemoteParticipants: Map<string, string>;
sortedRemoteVirtualScreenshareParticipants: Map<string, string>;
speakersList: Map<string, string>;
@ -243,7 +247,8 @@ ReducerRegistry.register<IParticipantsState>('features/base/participants',
fakeParticipant,
id,
name,
pinned
pinned,
sources
} = participant;
const { pinnedParticipant, dominantSpeaker } = state;
@ -287,6 +292,19 @@ ReducerRegistry.register<IParticipantsState>('features/base/participants',
state.remote.set(id, participant);
if (sources?.size) {
const videoSources: Map<string, ISourceInfo> | 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<IParticipantsState>('features/base/participants',
} = state;
let oldParticipant = remote.get(id);
if (oldParticipant?.sources?.size) {
const videoSources: Map<string, ISourceInfo> | 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<IParticipantsState>('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<string, ISourceInfo> = 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
};
}

View File

@ -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<string>} oldScreenshareSourceNames - List of old screenshare source names.
* @param {Array<string>} 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);
}

View File

@ -37,6 +37,7 @@ export interface IParticipant {
region?: string;
remoteControlSessionStatus?: boolean;
role?: string;
sources?: Map<string, Map<string, ISourceInfo>>;
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<string, Map<string, ISourceInfo>>;
isHidden: () => boolean;
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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<string>} participantList - The list of participants.
* @param {Object} state - The redux state.
* @returns {Array<string>}
*/
function _getSourceNames(participantList: Array<string>, state: IReduxState): Array<string> {
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);
}
}