Don't request video for invisible group call participants

This commit is contained in:
Evan Hahn 2021-12-06 17:06:13 -06:00 committed by GitHub
parent b4b65c4f00
commit 01549b11d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 171 additions and 16 deletions

View File

@ -27,7 +27,7 @@ import {
import { fakeGetGroupCallVideoFrameSource } from '../test-both/helpers/fakeGetGroupCallVideoFrameSource';
import enMessages from '../../_locales/en/messages.json';
const MAX_PARTICIPANTS = 32;
const MAX_PARTICIPANTS = 64;
const i18n = setupI18n('en', enMessages);
@ -301,7 +301,7 @@ story.add('Group call - Many', () => {
callMode: CallMode.Group,
remoteParticipants: allRemoteParticipants.slice(
0,
number('Participant count', 3, {
number('Participant count', 40, {
range: true,
min: 0,
max: MAX_PARTICIPANTS,

View File

@ -6,6 +6,7 @@ import React from 'react';
import { memoize, times } from 'lodash';
import { storiesOf } from '@storybook/react';
import { number } from '@storybook/addon-knobs';
import { action } from '@storybook/addon-actions';
import { GroupCallOverflowArea } from './GroupCallOverflowArea';
import { setupI18n } from '../util/setupI18n';
@ -37,6 +38,7 @@ const defaultProps = {
getFrameBuffer: memoize(() => new ArrayBuffer(FRAME_BUFFER_SIZE)),
getGroupCallVideoFrameSource: fakeGetGroupCallVideoFrameSource,
i18n,
onParticipantVisibilityChanged: action('onParticipantVisibilityChanged'),
};
// This component is usually rendered on a call screen.

View File

@ -19,6 +19,10 @@ type PropsType = {
getFrameBuffer: () => ArrayBuffer;
getGroupCallVideoFrameSource: (demuxId: number) => VideoFrameSource;
i18n: LocalizerType;
onParticipantVisibilityChanged: (
demuxId: number,
isVisible: boolean
) => unknown;
overflowedParticipants: ReadonlyArray<GroupCallRemoteParticipantType>;
};
@ -26,6 +30,7 @@ export const GroupCallOverflowArea: FC<PropsType> = ({
getFrameBuffer,
getGroupCallVideoFrameSource,
i18n,
onParticipantVisibilityChanged,
overflowedParticipants,
}) => {
const overflowRef = useRef<HTMLDivElement | null>(null);
@ -109,6 +114,7 @@ export const GroupCallOverflowArea: FC<PropsType> = ({
getFrameBuffer={getFrameBuffer}
getGroupCallVideoFrameSource={getGroupCallVideoFrameSource}
i18n={i18n}
onVisibilityChanged={onParticipantVisibilityChanged}
width={OVERFLOW_PARTICIPANT_WIDTH}
height={Math.floor(
OVERFLOW_PARTICIPANT_WIDTH / remoteParticipant.videoAspectRatio

View File

@ -29,6 +29,7 @@ type BasePropsType = {
getFrameBuffer: () => ArrayBuffer;
getGroupCallVideoFrameSource: (demuxId: number) => VideoFrameSource;
i18n: LocalizerType;
onVisibilityChanged?: (demuxId: number, isVisible: boolean) => unknown;
remoteParticipant: GroupCallRemoteParticipantType;
};
@ -52,7 +53,12 @@ export type PropsType = BasePropsType &
export const GroupCallRemoteParticipant: React.FC<PropsType> = React.memo(
props => {
const { getFrameBuffer, getGroupCallVideoFrameSource, i18n } = props;
const {
getFrameBuffer,
getGroupCallVideoFrameSource,
i18n,
onVisibilityChanged,
} = props;
const {
acceptedMessageRequest,
@ -95,6 +101,10 @@ export const GroupCallRemoteParticipant: React.FC<PropsType> = React.memo(
? intersectionObserverEntry.isIntersecting
: true;
useEffect(() => {
onVisibilityChanged?.(demuxId, isVisible);
}, [demuxId, isVisible, onVisibilityChanged]);
const wantsToShowVideo = hasRemoteVideo && !isBlocked && isVisible;
const hasVideoToShow = wantsToShowVideo && hasReceivedVideoRecently;

View File

@ -1,7 +1,7 @@
// Copyright 2020-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useState, useMemo, useEffect } from 'react';
import React, { useCallback, useState, useMemo, useEffect } from 'react';
import Measure from 'react-measure';
import { takeWhile, chunk, maxBy, flatten, noop } from 'lodash';
import type { VideoFrameSource } from 'ringrtc';
@ -20,6 +20,8 @@ import { usePageVisibility } from '../hooks/usePageVisibility';
import { nonRenderedRemoteParticipant } from '../util/ringrtc/nonRenderedRemoteParticipant';
import { missingCaseError } from '../util/missingCaseError';
import { SECOND } from '../util/durations';
import { filter } from '../util/iterables';
import * as setUtil from '../util/setUtil';
import * as log from '../logging/log';
const MIN_RENDERED_HEIGHT = 180;
@ -94,6 +96,9 @@ export const GroupCallRemoteParticipants: React.FC<PropsType> = ({
const getFrameBuffer = useGetCallingFrameBuffer();
const { invisibleDemuxIds, onParticipantVisibilityChanged } =
useInvisibleParticipants(remoteParticipants);
// 1. Figure out the maximum number of possible rows that could fit on the screen.
//
// We choose the smaller of these two options:
@ -310,19 +315,23 @@ export const GroupCallRemoteParticipants: React.FC<PropsType> = ({
};
}),
...overflowedParticipants.map(participant => {
if (participant.hasRemoteVideo) {
return {
demuxId: participant.demuxId,
width: Math.floor(
OVERFLOW_PARTICIPANT_WIDTH * VIDEO_REQUEST_SCALAR
),
height: Math.floor(
(OVERFLOW_PARTICIPANT_WIDTH / participant.videoAspectRatio) *
VIDEO_REQUEST_SCALAR
),
};
if (
!participant.hasRemoteVideo ||
invisibleDemuxIds.has(participant.demuxId)
) {
return nonRenderedRemoteParticipant(participant);
}
return nonRenderedRemoteParticipant(participant);
return {
demuxId: participant.demuxId,
width: Math.floor(
OVERFLOW_PARTICIPANT_WIDTH * VIDEO_REQUEST_SCALAR
),
height: Math.floor(
(OVERFLOW_PARTICIPANT_WIDTH / participant.videoAspectRatio) *
VIDEO_REQUEST_SCALAR
),
};
}),
];
break;
@ -350,6 +359,7 @@ export const GroupCallRemoteParticipants: React.FC<PropsType> = ({
}, [
gridParticipantHeight,
videoRequestMode,
invisibleDemuxIds,
overflowedParticipants,
remoteParticipants,
setGroupCallVideoRequest,
@ -396,6 +406,7 @@ export const GroupCallRemoteParticipants: React.FC<PropsType> = ({
getFrameBuffer={getFrameBuffer}
getGroupCallVideoFrameSource={getGroupCallVideoFrameSource}
i18n={i18n}
onParticipantVisibilityChanged={onParticipantVisibilityChanged}
overflowedParticipants={overflowedParticipants}
/>
</div>
@ -404,6 +415,54 @@ export const GroupCallRemoteParticipants: React.FC<PropsType> = ({
);
};
// This function is only meant for use with `useInvisibleParticipants`. It helps avoid
// returning new set instances when the underlying values are equal.
function pickDifferentSet<T>(a: Readonly<Set<T>>, b: Readonly<Set<T>>): Set<T> {
return a.size === b.size ? a : b;
}
function useInvisibleParticipants(
remoteParticipants: ReadonlyArray<GroupCallRemoteParticipantType>
): Readonly<{
invisibleDemuxIds: Set<number>;
onParticipantVisibilityChanged: (demuxId: number, isVisible: boolean) => void;
}> {
const [invisibleDemuxIds, setInvisibleDemuxIds] = useState(new Set<number>());
const onParticipantVisibilityChanged = useCallback(
(demuxId: number, isVisible: boolean) => {
setInvisibleDemuxIds(oldInvisibleDemuxIds => {
const toggled = setUtil.toggle(
oldInvisibleDemuxIds,
demuxId,
!isVisible
);
return pickDifferentSet(oldInvisibleDemuxIds, toggled);
});
},
[]
);
useEffect(() => {
const remoteParticipantDemuxIds = new Set<number>(
remoteParticipants.map(r => r.demuxId)
);
setInvisibleDemuxIds(oldInvisibleDemuxIds => {
const staleIds = filter(
oldInvisibleDemuxIds,
id => !remoteParticipantDemuxIds.has(id)
);
const withoutStaleIds = setUtil.remove(oldInvisibleDemuxIds, ...staleIds);
return pickDifferentSet(oldInvisibleDemuxIds, withoutStaleIds);
});
}, [remoteParticipants]);
return {
invisibleDemuxIds,
onParticipantVisibilityChanged,
};
}
function useVideoRequestMode(): VideoRequestMode {
const isPageVisible = usePageVisibility();

View File

@ -0,0 +1,56 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import { remove, toggle } from '../../util/setUtil';
describe('set utilities', () => {
const original = new Set([1, 2, 3]);
describe('remove', () => {
it('accepts zero arguments, returning a new set', () => {
const result = remove(original);
assert.deepStrictEqual(result, original);
assert.notStrictEqual(result, original);
});
it('accepts 1 argument, returning a new set', () => {
const result = remove(original, 2);
assert.deepStrictEqual(result, new Set([1, 3]));
assert.deepStrictEqual(original, new Set([1, 2, 3]));
});
it('accepts multiple arguments, returning a new set', () => {
const result = remove(original, 1, 2, 99);
assert.deepStrictEqual(result, new Set([3]));
assert.deepStrictEqual(original, new Set([1, 2, 3]));
});
});
describe('toggle', () => {
it('returns a clone if trying to remove an item that was never there', () => {
const result = toggle(original, 99, false);
assert.deepStrictEqual(result, new Set([1, 2, 3]));
assert.notStrictEqual(result, original);
});
it('returns a clone if trying to add an item that was already there', () => {
const result = toggle(original, 3, true);
assert.deepStrictEqual(result, new Set([1, 2, 3]));
assert.notStrictEqual(result, original);
});
it('can add an item to a set', () => {
const result = toggle(original, 4, true);
assert.deepStrictEqual(result, new Set([1, 2, 3, 4]));
assert.deepStrictEqual(original, new Set([1, 2, 3]));
});
it('can remove an item from a set', () => {
const result = toggle(original, 2, false);
assert.deepStrictEqual(result, new Set([1, 3]));
assert.deepStrictEqual(original, new Set([1, 2, 3]));
});
});
});

22
ts/util/setUtil.ts Normal file
View File

@ -0,0 +1,22 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
const add = <T>(set: Readonly<Set<T>>, item: T): Set<T> =>
new Set(set).add(item);
export const remove = <T>(
set: Readonly<Set<T>>,
...items: ReadonlyArray<T>
): Set<T> => {
const clone = new Set(set);
for (const item of items) {
clone.delete(item);
}
return clone;
};
export const toggle = <T>(
set: Readonly<Set<T>>,
item: Readonly<T>,
shouldInclude: boolean
): Set<T> => (shouldInclude ? add : remove)(set, item);