feat: request video only for visible thumbnails.

This commit is contained in:
Hristo Terezov 2021-05-19 18:23:40 -05:00
parent 16cfda3c7a
commit 2d319d18c3
14 changed files with 209 additions and 127 deletions

View File

@ -70,3 +70,15 @@ export const SET_VERTICAL_VIEW_DIMENSIONS = 'SET_VERTICAL_VIEW_DIMENSIONS';
* } * }
*/ */
export const SET_VOLUME = 'SET_VOLUME'; export const SET_VOLUME = 'SET_VOLUME';
/**
* The type of the action which sets the list of visible remote participants in the filmstrip by storing the start and
* end index in the remote participants array.
*
* {
* type: SET_VISIBLE_REMOTE_PARTICIPANTS,
* startIndex: number,
* endIndex: number
* }
*/
export const SET_VISIBLE_REMOTE_PARTICIPANTS = 'SET_VISIBLE_REMOTE_PARTICIPANTS';

View File

@ -7,6 +7,7 @@ import {
SET_HORIZONTAL_VIEW_DIMENSIONS, SET_HORIZONTAL_VIEW_DIMENSIONS,
SET_TILE_VIEW_DIMENSIONS, SET_TILE_VIEW_DIMENSIONS,
SET_VERTICAL_VIEW_DIMENSIONS, SET_VERTICAL_VIEW_DIMENSIONS,
SET_VISIBLE_REMOTE_PARTICIPANTS,
SET_VOLUME SET_VOLUME
} from './actionTypes'; } from './actionTypes';
import { import {
@ -153,4 +154,24 @@ export function setVolume(participantId: string, volume: number) {
}; };
} }
/**
* Sets the list of the visible participants in the filmstrip by storing the start and end index from the remote
* participants array.
*
* @param {number} startIndex - The start index from the remote participants array.
* @param {number} endIndex - The end index from the remote participants array.
* @returns {{
* type: SET_VISIBLE_REMOTE_PARTICIPANTS,
* startIndex: number,
* endIndex: number
* }}
*/
export function setVisibleRemoteParticipants(startIndex: number, endIndex: number) {
return {
type: SET_VISIBLE_REMOTE_PARTICIPANTS,
startIndex,
endIndex
};
}
export * from './actions.native'; export * from './actions.native';

View File

@ -16,7 +16,7 @@ import { connect } from '../../../base/redux';
import { showToolbox } from '../../../toolbox/actions.web'; import { showToolbox } from '../../../toolbox/actions.web';
import { isButtonEnabled, isToolboxVisible } from '../../../toolbox/functions.web'; import { isButtonEnabled, isToolboxVisible } from '../../../toolbox/functions.web';
import { LAYOUTS, getCurrentLayout } from '../../../video-layout'; import { LAYOUTS, getCurrentLayout } from '../../../video-layout';
import { setFilmstripVisible } from '../../actions'; import { setFilmstripVisible, setVisibleRemoteParticipants } from '../../actions';
import { TILE_HORIZONTAL_MARGIN, TILE_VERTICAL_MARGIN, TOOLBAR_HEIGHT } from '../../constants'; import { TILE_HORIZONTAL_MARGIN, TILE_VERTICAL_MARGIN, TOOLBAR_HEIGHT } from '../../constants';
import { shouldRemoteVideosBeVisible } from '../../functions'; import { shouldRemoteVideosBeVisible } from '../../functions';
@ -67,7 +67,6 @@ type Props = {
*/ */
_remoteParticipants: Array<Object>, _remoteParticipants: Array<Object>,
/** /**
* The length of the remote participants array. * The length of the remote participants array.
*/ */
@ -137,6 +136,8 @@ class Filmstrip extends PureComponent <Props> {
this._onTabIn = this._onTabIn.bind(this); this._onTabIn = this._onTabIn.bind(this);
this._gridItemKey = this._gridItemKey.bind(this); this._gridItemKey = this._gridItemKey.bind(this);
this._listItemKey = this._listItemKey.bind(this); this._listItemKey = this._listItemKey.bind(this);
this._onGridItemsRendered = this._onGridItemsRendered.bind(this);
this._onListItemsRendered = this._onListItemsRendered.bind(this);
} }
/** /**
@ -268,6 +269,41 @@ class Filmstrip extends PureComponent <Props> {
return _remoteParticipants[index]; return _remoteParticipants[index];
} }
_onListItemsRendered: Object => void;
/**
* Handles items rendered changes in stage view.
*
* @param {Object} data - Information about the rendered items.
* @returns {void}
*/
_onListItemsRendered({ overscanStartIndex, overscanStopIndex }) {
const { dispatch } = this.props;
dispatch(setVisibleRemoteParticipants(overscanStartIndex, overscanStopIndex));
}
_onGridItemsRendered: Object => void;
/**
* Handles items rendered changes in tile view.
*
* @param {Object} data - Information about the rendered items.
* @returns {void}
*/
_onGridItemsRendered({
overscanColumnStartIndex,
overscanColumnStopIndex,
overscanRowStartIndex,
overscanRowStopIndex
}) {
const { _columns, dispatch } = this.props;
const startIndex = (overscanRowStartIndex * _columns) + overscanColumnStartIndex;
const endIndex = (overscanRowStopIndex * _columns) + overscanColumnStopIndex;
dispatch(setVisibleRemoteParticipants(startIndex, endIndex));
}
/** /**
* Renders the thumbnails for remote participants. * Renders the thumbnails for remote participants.
* *
@ -301,6 +337,7 @@ class Filmstrip extends PureComponent <Props> {
initialScrollLeft = { 0 } initialScrollLeft = { 0 }
initialScrollTop = { 0 } initialScrollTop = { 0 }
itemKey = { this._gridItemKey } itemKey = { this._gridItemKey }
onItemsRendered = { this._onGridItemsRendered }
rowCount = { _rows } rowCount = { _rows }
rowHeight = { _thumbnailHeight + TILE_VERTICAL_MARGIN } rowHeight = { _thumbnailHeight + TILE_VERTICAL_MARGIN }
width = { _filmstripWidth }> width = { _filmstripWidth }>
@ -318,6 +355,7 @@ class Filmstrip extends PureComponent <Props> {
height: _filmstripHeight, height: _filmstripHeight,
itemKey: this._listItemKey, itemKey: this._listItemKey,
itemSize: 0, itemSize: 0,
onItemsRendered: this._onListItemsRendered,
width: _filmstripWidth, width: _filmstripWidth,
style: { style: {
willChange: 'auto' willChange: 'auto'

View File

@ -12,16 +12,16 @@ import Thumbnail from './Thumbnail';
*/ */
type Props = { type Props = {
/**
* The horizontal offset in px for the thumbnail. Used to center the thumbnails in the last row in tile view.
*/
_horizontalOffset: number,
/** /**
* The ID of the participant associated with the Thumbnail. * The ID of the participant associated with the Thumbnail.
*/ */
_participantID: ?string, _participantID: ?string,
/**
* The horizontal offset in px for the thumbnail. Used to center the thumbnails in the last row in tile view.
*/
_horizontalOffset: number,
/** /**
* The index of the column in tile view. * The index of the column in tile view.
*/ */
@ -114,12 +114,11 @@ function _mapStateToProps(state, ownProps) {
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 participantsInTheLastRow = (remoteParticipantsLength + 1) % columns; const partialLastRowParticipantsNumber = (remoteParticipantsLength + 1) % columns;
if (participantsInTheLastRow > 0) { if (partialLastRowParticipantsNumber > 0) {
horizontalOffset = Math.floor((columns - participantsInTheLastRow) * (thumbnailWidth + 4) / 2); horizontalOffset = Math.floor((columns - partialLastRowParticipantsNumber) * (thumbnailWidth + 4) / 2);
} }
} }
if (index > remoteParticipantsLength) { if (index > remoteParticipantsLength) {
@ -133,12 +132,10 @@ function _mapStateToProps(state, ownProps) {
}; };
} }
return { return {
_participantID: remoteParticipants[index], _participantID: remoteParticipants[index],
_horizontalOffset: horizontalOffset _horizontalOffset: horizontalOffset
}; };
} }
const { index } = ownProps; const { index } = ownProps;

View File

@ -9,6 +9,7 @@ import {
SET_HORIZONTAL_VIEW_DIMENSIONS, SET_HORIZONTAL_VIEW_DIMENSIONS,
SET_TILE_VIEW_DIMENSIONS, SET_TILE_VIEW_DIMENSIONS,
SET_VERTICAL_VIEW_DIMENSIONS, SET_VERTICAL_VIEW_DIMENSIONS,
SET_VISIBLE_REMOTE_PARTICIPANTS,
SET_VOLUME SET_VOLUME
} from './actionTypes'; } from './actionTypes';
@ -66,7 +67,32 @@ const DEFAULT_STATE = {
* @public * @public
* @type {boolean} * @type {boolean}
*/ */
visible: true visible: true,
/**
* The end index in the remote participants array that is visible in the filmstrip.
*
* @public
* @type {number}
*/
visibleParticipantsEndIndex: 0,
/**
* The visible participants in the filmstrip.
*
* @public
* @type {Array<string>}
*/
visibleParticipants: [],
/**
* The start index in the remote participants array that is visible in the filmstrip.
*
* @public
* @type {number}
*/
visibleParticipantsStartIndex: 0
}; };
ReducerRegistry.register( ReducerRegistry.register(
@ -112,11 +138,24 @@ ReducerRegistry.register(
[action.participantId]: action.volume [action.participantId]: action.volume
} }
}; };
case SET_VISIBLE_REMOTE_PARTICIPANTS:
return {
...state,
visibleParticipantsStartIndex: action.startIndex,
visibleParticipantsEndIndex: action.endIndex,
visibleParticipants: state.remoteParticipants.slice(action.startIndex, action.endIndex + 1)
};
case PARTICIPANT_JOINED: { case PARTICIPANT_JOINED: {
const { id, local } = action.participant; const { id, local } = action.participant;
if (!local) { if (!local) {
state.remoteParticipants = [ ...state.remoteParticipants, id ]; state.remoteParticipants = [ ...state.remoteParticipants, id ];
const { visibleParticipantsStartIndex: startIndex, visibleParticipantsEndIndex: endIndex } = state;
if (state.remoteParticipants.length - 1 <= endIndex) {
state.visibleParticipants = state.remoteParticipants.slice(startIndex, endIndex + 1);
}
} }
return state; return state;
@ -128,7 +167,24 @@ ReducerRegistry.register(
return state; return state;
} }
state.remoteParticipants = state.remoteParticipants.filter(participantId => participantId !== id); let removedParticipantIndex = 0;
state.remoteParticipants = state.remoteParticipants.filter((participantId, index) => {
if (participantId === id) {
removedParticipantIndex = index;
return false;
}
return true;
});
const { visibleParticipantsStartIndex: startIndex, visibleParticipantsEndIndex: endIndex } = state;
if (removedParticipantIndex >= startIndex && removedParticipantIndex <= endIndex) {
state.visibleParticipants = state.remoteParticipants.slice(startIndex, endIndex + 1);
}
delete state.participantsVolume[id]; delete state.participantsVolume[id];
return state; return state;

View File

@ -3,30 +3,12 @@
import type { Dispatch } from 'redux'; import type { Dispatch } from 'redux';
import { MEDIA_TYPE } from '../base/media'; import { MEDIA_TYPE } from '../base/media';
import { getParticipants } from '../base/participants';
import { selectEndpoints, shouldDisplayTileView } from '../video-layout';
import { import {
SELECT_LARGE_VIDEO_PARTICIPANT, SELECT_LARGE_VIDEO_PARTICIPANT,
UPDATE_KNOWN_LARGE_VIDEO_RESOLUTION UPDATE_KNOWN_LARGE_VIDEO_RESOLUTION
} from './actionTypes'; } from './actionTypes';
/**
* Signals conference to select a participant.
*
* @returns {Function}
*/
export function selectParticipant() {
return (dispatch: Dispatch<any>, getState: Function) => {
const state = getState();
const ids = shouldDisplayTileView(state)
? getParticipants(state).map(participant => participant.id)
: [ state['features/large-video'].participantId ];
dispatch(selectEndpoints(ids));
};
}
/** /**
* Action to select the participant to be displayed in LargeVideo based on the * Action to select the participant to be displayed in LargeVideo based on the
* participant id provided. If a participant id is not provided, the LargeVideo * participant id provided. If a participant id is not provided, the LargeVideo
@ -60,8 +42,6 @@ export function selectParticipantInLargeVideo(participant: ?string) {
type: SELECT_LARGE_VIDEO_PARTICIPANT, type: SELECT_LARGE_VIDEO_PARTICIPANT,
participantId participantId
}); });
dispatch(selectParticipant());
} }
}; };
} }

View File

@ -1,6 +1,5 @@
// @flow // @flow
import { CONFERENCE_JOINED } from '../base/conference';
import { import {
DOMINANT_SPEAKER_CHANGED, DOMINANT_SPEAKER_CHANGED,
PARTICIPANT_JOINED, PARTICIPANT_JOINED,
@ -11,13 +10,11 @@ import {
import { MiddlewareRegistry } from '../base/redux'; import { MiddlewareRegistry } from '../base/redux';
import { isTestModeEnabled } from '../base/testing'; import { isTestModeEnabled } from '../base/testing';
import { import {
getTrackByJitsiTrack,
TRACK_ADDED, TRACK_ADDED,
TRACK_REMOVED, TRACK_REMOVED
TRACK_UPDATED
} from '../base/tracks'; } from '../base/tracks';
import { selectParticipant, selectParticipantInLargeVideo } from './actions.any'; import { selectParticipantInLargeVideo } from './actions';
import logger from './logger'; import logger from './logger';
import './subscriber'; import './subscriber';
@ -54,30 +51,6 @@ MiddlewareRegistry.register(store => next => action => {
case TRACK_REMOVED: case TRACK_REMOVED:
store.dispatch(selectParticipantInLargeVideo()); store.dispatch(selectParticipantInLargeVideo());
break; break;
case CONFERENCE_JOINED:
// Ensure a participant is selected on conference join. This addresses
// the case where video tracks were received before CONFERENCE_JOINED
// fired; without the conference selection may not happen.
store.dispatch(selectParticipant());
break;
case TRACK_UPDATED:
// In order to minimize re-calculations, we need to select participant
// only if the videoType of the current participant rendered in
// LargeVideo has changed.
if ('videoType' in action.track) {
const state = store.getState();
const track
= getTrackByJitsiTrack(
state['features/base/tracks'],
action.track.jitsiTrack);
const participantId = state['features/large-video'].participantId;
(track.participantId === participantId)
&& store.dispatch(selectParticipant());
}
break;
} }
return result; return result;

View File

@ -1363,6 +1363,7 @@ class Toolbox extends Component<Props> {
{ showOverflowMenuButton && <OverflowMenuButton { showOverflowMenuButton && <OverflowMenuButton
ariaControls = 'overflow-menu' ariaControls = 'overflow-menu'
isOpen = { _overflowMenuVisible } isOpen = { _overflowMenuVisible }
key = 'overflow-menu'
onVisibilityChange = { this._onSetOverflowVisible }> onVisibilityChange = { this._onSetOverflowVisible }>
<ul <ul
aria-label = { t(toolbarAccLabel) } aria-label = { t(toolbarAccLabel) }
@ -1375,6 +1376,7 @@ class Toolbox extends Component<Props> {
</OverflowMenuButton>} </OverflowMenuButton>}
<HangupButton <HangupButton
customClass = 'hangup-button' customClass = 'hangup-button'
key = 'hangup-button'
visible = { this.props._shouldShowButton('hangup') } /> visible = { this.props._shouldShowButton('hangup') } />
</div> </div>
</div> </div>

View File

@ -10,12 +10,6 @@
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 the endpoints to be selected for video forwarding
* from the bridge.
*/
export const SELECT_ENDPOINTS = 'SELECT_ENDPOINTS';
/** /**
* 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

@ -4,28 +4,10 @@ import type { Dispatch } from 'redux';
import { import {
SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED, SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED,
SELECT_ENDPOINTS,
SET_TILE_VIEW SET_TILE_VIEW
} from './actionTypes'; } from './actionTypes';
import { shouldDisplayTileView } from './functions'; import { shouldDisplayTileView } from './functions';
/**
* Creates a (redux) action which signals that a new set of remote endpoints need to be selected.
*
* @param {Array<string>} participantIds - The remote participants that are currently selected
* for video forwarding from the bridge.
* @returns {{
* type: SELECT_ENDPOINTS,
* particpantsIds: Array<string>
* }}
*/
export function selectEndpoints(participantIds: Array<string>) {
return {
type: SELECT_ENDPOINTS,
participantIds
};
}
/** /**
* Creates a (redux) action which signals that the list of known remote participants * Creates a (redux) action which signals that the list of known remote participants
* with screen shares has changed. * with screen shares has changed.

View File

@ -4,7 +4,6 @@ import { ReducerRegistry } from '../base/redux';
import { import {
SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED, SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED,
SELECT_ENDPOINTS,
SET_TILE_VIEW SET_TILE_VIEW
} from './actionTypes'; } from './actionTypes';
@ -35,13 +34,6 @@ ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STATE, action) => {
}; };
} }
case SELECT_ENDPOINTS: {
return {
...state,
selectedEndpoints: action.participantIds
};
}
case SET_TILE_VIEW: case SET_TILE_VIEW:
return { return {
...state, ...state,

View File

@ -4,24 +4,10 @@ import debounce from 'lodash/debounce';
import { StateListenerRegistry, equals } from '../base/redux'; import { StateListenerRegistry, equals } from '../base/redux';
import { isFollowMeActive } from '../follow-me'; import { isFollowMeActive } from '../follow-me';
import { selectParticipant } from '../large-video/actions.any';
import { setRemoteParticipantsWithScreenShare } from './actions'; import { setRemoteParticipantsWithScreenShare } from './actions';
import { getAutoPinSetting, updateAutoPinnedParticipant } from './functions'; import { getAutoPinSetting, updateAutoPinnedParticipant } from './functions';
/**
* StateListenerRegistry provides a reliable way of detecting changes to
* preferred layout state and dispatching additional actions.
*/
StateListenerRegistry.register(
/* selector */ state => state['features/video-layout'].tileViewEnabled,
/* listener */ (tileViewEnabled, store) => {
const { dispatch } = store;
dispatch(selectParticipant());
}
);
/** /**
* 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

View File

@ -14,7 +14,8 @@ export const VIDEO_QUALITY_LEVELS = {
ULTRA: 2160, ULTRA: 2160,
HIGH: 720, HIGH: 720,
STANDARD: 360, STANDARD: 360,
LOW: 180 LOW: 180,
NONE: 0
}; };
/** /**

View File

@ -17,16 +17,47 @@ import { getMinHeightForQualityLvlMap } from './selector';
declare var APP: Object; declare var APP: Object;
/** /**
* StateListenerRegistry provides a reliable way of detecting changes to selected * Handles changes in the visible participants in the filmstrip. The listener is debounced
* endpoints state and dispatching additional actions. The listener is debounced
* so that the client doesn't end up sending too many bridge messages when the user is * so that the client doesn't end up sending too many bridge messages when the user is
* scrolling through the thumbnails prompting updates to the selected endpoints. * scrolling through the thumbnails prompting updates to the selected endpoints.
*/ */
StateListenerRegistry.register( StateListenerRegistry.register(
/* selector */ state => state['features/video-layout'].selectedEndpoints, /* selector */ state => state['features/filmstrip'].visibleParticipants,
/* listener */ debounce((selectedEndpoints, store) => { /* listener */ debounce((visibleParticipants, store) => {
_updateReceiverVideoConstraints(store); _updateReceiverVideoConstraints(store);
}, 1000)); }, 100));
/**
* Handles the use case when the on-stage participant has changed.
*/
StateListenerRegistry.register(
state => state['features/large-video'].participantId,
(participantId, store) => {
_updateReceiverVideoConstraints(store);
}
);
/**
* Handles the use case when we have set some of the constraints in redux but the conference object wasn't available
* and we haven't been able to pass the constraints to lib-jitsi-meet.
*/
StateListenerRegistry.register(
state => state['features/base/conference'].conference,
(conference, store) => {
_updateReceiverVideoConstraints(store);
}
);
/**
* Updates the receiver constraints when the layout changes. When we are in stage view we need to handle the
* on-stage participant differently.
*/
StateListenerRegistry.register(
/* selector */ state => state['features/video-layout'].tileViewEnabled,
/* listener */ (tileViewEnabled, store) => {
_updateReceiverVideoConstraints(store);
}
);
/** /**
* StateListenerRegistry provides a reliable way of detecting changes to * StateListenerRegistry provides a reliable way of detecting changes to
@ -64,6 +95,8 @@ StateListenerRegistry.register(
typeof APP !== 'undefined' && APP.API.notifyVideoQualityChanged(preferredVideoQuality); typeof APP !== 'undefined' && APP.API.notifyVideoQualityChanged(preferredVideoQuality);
} }
changedReceiverVideoQuality && _updateReceiverVideoConstraints(store); changedReceiverVideoQuality && _updateReceiverVideoConstraints(store);
}, {
deepEquals: true
}); });
/** /**
@ -156,28 +189,43 @@ function _updateReceiverVideoConstraints({ getState }) {
} }
const { lastN } = state['features/base/lastn']; const { lastN } = state['features/base/lastn'];
const { maxReceiverVideoQuality, preferredVideoQuality } = state['features/video-quality']; const { maxReceiverVideoQuality, preferredVideoQuality } = state['features/video-quality'];
const { selectedEndpoints } = state['features/video-layout']; const { visibleParticipants } = state['features/filmstrip'];
const { participantId: largeVideoParticipantId } = state['features/large-video'];
const maxFrameHeight = Math.min(maxReceiverVideoQuality, preferredVideoQuality); const maxFrameHeight = Math.min(maxReceiverVideoQuality, preferredVideoQuality);
const receiverConstraints = { const receiverConstraints = {
constraints: {}, constraints: {},
defaultConstraints: { 'maxHeight': VIDEO_QUALITY_LEVELS.LOW }, defaultConstraints: { 'maxHeight': VIDEO_QUALITY_LEVELS.NONE },
lastN, lastN,
onStageEndpoints: [], onStageEndpoints: [],
selectedEndpoints: [] selectedEndpoints: []
}; };
if (!selectedEndpoints?.length) { // Tile view.
return; if (shouldDisplayTileView(state)) {
} if (!visibleParticipants?.length) {
return;
}
visibleParticipants.forEach(participantId => {
receiverConstraints.constraints[participantId] = { 'maxHeight': maxFrameHeight };
});
// Stage view. // Stage view.
if (selectedEndpoints?.length === 1) {
receiverConstraints.constraints[selectedEndpoints[0]] = { 'maxHeight': maxFrameHeight };
receiverConstraints.onStageEndpoints = selectedEndpoints;
// Tile view.
} else { } else {
receiverConstraints.defaultConstraints = { 'maxHeight': maxFrameHeight }; if (!visibleParticipants?.length && !largeVideoParticipantId) {
return;
}
if (visibleParticipants?.length > 0) {
visibleParticipants.forEach(participantId => {
receiverConstraints.constraints[participantId] = { 'maxHeight': VIDEO_QUALITY_LEVELS.LOW };
});
}
if (largeVideoParticipantId) {
receiverConstraints.constraints[largeVideoParticipantId] = { 'maxHeight': maxFrameHeight };
receiverConstraints.onStageEndpoints = [ largeVideoParticipantId ];
}
} }
logger.info(`Setting receiver video constraints to ${JSON.stringify(receiverConstraints)}`); logger.info(`Setting receiver video constraints to ${JSON.stringify(receiverConstraints)}`);