Group calling: tell RingRTC about our rendered resolutions for perf

This commit is contained in:
Evan Hahn 2020-12-01 19:52:01 -06:00 committed by GitHub
parent b30b83ed57
commit d1866a0e5d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 211 additions and 7 deletions

View file

@ -6552,7 +6552,7 @@ button.module-image__border-overlay:focus {
background-color: $color-gray-95;
border-radius: 4px 4px 0 0;
display: flex;
height: 120px;
height: 120px; // This height should be kept in sync with <CallingPipRemoteVideo>'s hard-coded height.
justify-content: center;
overflow: hidden;
position: relative;

View file

@ -73,6 +73,7 @@ const createProps = (storyProps: Partial<PropsType> = {}): PropsType => ({
title: text('Caller Title', 'Morty Smith'),
},
renderDeviceSelection: () => <div />,
setGroupCallVideoRequest: action('set-group-call-video-request'),
setLocalAudio: action('set-local-audio'),
setLocalPreview: action('set-local-preview'),
setLocalVideo: action('set-local-video'),

View file

@ -13,6 +13,7 @@ import {
CallMode,
CallState,
GroupCallJoinState,
GroupCallVideoRequest,
VideoFrameSource,
} from '../types/Calling';
import { ConversationType } from '../state/ducks/conversations';
@ -23,6 +24,7 @@ import {
DeclineCallType,
DirectCallStateType,
HangUpType,
SetGroupCallVideoRequestType,
SetLocalAudioType,
SetLocalPreviewType,
SetLocalVideoType,
@ -60,6 +62,7 @@ export interface PropsType {
profileName?: string;
title: string;
};
setGroupCallVideoRequest: (_: SetGroupCallVideoRequestType) => void;
setLocalAudio: (_: SetLocalAudioType) => void;
setLocalVideo: (_: SetLocalVideoType) => void;
setLocalPreview: (_: SetLocalPreviewType) => void;
@ -83,6 +86,7 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
getGroupCallVideoFrameSource,
me,
renderDeviceSelection,
setGroupCallVideoRequest,
setLocalAudio,
setLocalPreview,
setLocalVideo,
@ -129,6 +133,16 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
[getGroupCallVideoFrameSource, conversation.id]
);
const setGroupCallVideoRequestForConversation = useCallback(
(resolutions: Array<GroupCallVideoRequest>) => {
setGroupCallVideoRequest({
conversationId: conversation.id,
resolutions,
});
},
[setGroupCallVideoRequest, conversation.id]
);
let showCallLobby: boolean;
switch (call.callMode) {
@ -205,6 +219,7 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
hangUp={hangUp}
hasLocalVideo={hasLocalVideo}
i18n={i18n}
setGroupCallVideoRequest={setGroupCallVideoRequestForConversation}
setLocalPreview={setLocalPreview}
setRendererCanvas={setRendererCanvas}
togglePip={togglePip}
@ -223,6 +238,7 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
i18n={i18n}
joinedAt={joinedAt}
me={me}
setGroupCallVideoRequest={setGroupCallVideoRequestForConversation}
setLocalPreview={setLocalPreview}
setRendererCanvas={setRendererCanvas}
setLocalAudio={setLocalAudio}

View file

@ -114,6 +114,7 @@ const createProps = (
profileName: 'Morty Smith',
title: 'Morty Smith',
},
setGroupCallVideoRequest: action('set-group-call-video-request'),
setLocalAudio: action('set-local-audio'),
setLocalPreview: action('set-local-preview'),
setLocalVideo: action('set-local-video'),

View file

@ -20,6 +20,7 @@ import {
CallMode,
CallState,
GroupCallConnectionState,
GroupCallVideoRequest,
VideoFrameSource,
} from '../types/Calling';
import { ColorType } from '../types/Colors';
@ -45,6 +46,7 @@ export type PropsType = {
profileName?: string;
title: string;
};
setGroupCallVideoRequest: (_: Array<GroupCallVideoRequest>) => void;
setLocalAudio: (_: SetLocalAudioType) => void;
setLocalVideo: (_: SetLocalVideoType) => void;
setLocalPreview: (_: SetLocalPreviewType) => void;
@ -64,6 +66,7 @@ export const CallScreen: React.FC<PropsType> = ({
i18n,
joinedAt,
me,
setGroupCallVideoRequest,
setLocalAudio,
setLocalVideo,
setLocalPreview,
@ -187,6 +190,7 @@ export const CallScreen: React.FC<PropsType> = ({
getGroupCallVideoFrameSource={getGroupCallVideoFrameSource}
i18n={i18n}
remoteParticipants={groupCallParticipants}
setGroupCallVideoRequest={setGroupCallVideoRequest}
/>
);
break;

View file

@ -68,6 +68,7 @@ const createProps = (
hangUp: action('hang-up'),
hasLocalVideo: boolean('hasLocalVideo', overrideProps.hasLocalVideo || false),
i18n,
setGroupCallVideoRequest: action('set-group-call-video-request'),
setLocalPreview: action('set-local-preview'),
setRendererCanvas: action('set-renderer-canvas'),
togglePip: action('toggle-pip'),

View file

@ -5,7 +5,7 @@ import React from 'react';
import { minBy, debounce, noop } from 'lodash';
import { CallingPipRemoteVideo } from './CallingPipRemoteVideo';
import { LocalizerType } from '../types/Util';
import { VideoFrameSource } from '../types/Calling';
import { GroupCallVideoRequest, VideoFrameSource } from '../types/Calling';
import {
ActiveCallType,
HangUpType,
@ -54,6 +54,7 @@ export type PropsType = {
hangUp: (_: HangUpType) => void;
hasLocalVideo: boolean;
i18n: LocalizerType;
setGroupCallVideoRequest: (_: Array<GroupCallVideoRequest>) => void;
setLocalPreview: (_: SetLocalPreviewType) => void;
setRendererCanvas: (_: SetRendererCanvasType) => void;
togglePip: () => void;
@ -70,6 +71,7 @@ export const CallingPip = ({
hangUp,
hasLocalVideo,
i18n,
setGroupCallVideoRequest,
setLocalPreview,
setRendererCanvas,
togglePip,
@ -269,6 +271,7 @@ export const CallingPip = ({
getGroupCallVideoFrameSource={getGroupCallVideoFrameSource}
i18n={i18n}
setRendererCanvas={setRendererCanvas}
setGroupCallVideoRequest={setGroupCallVideoRequest}
/>
{hasLocalVideo ? (
<video

View file

@ -1,7 +1,7 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useMemo } from 'react';
import React, { useMemo, useEffect } from 'react';
import { maxBy } from 'lodash';
import { Avatar } from './Avatar';
import { CallBackgroundBlur } from './CallBackgroundBlur';
@ -11,9 +11,15 @@ import { LocalizerType } from '../types/Util';
import {
CallMode,
GroupCallRemoteParticipantType,
GroupCallVideoRequest,
VideoFrameSource,
} from '../types/Calling';
import { ActiveCallType, SetRendererCanvasType } from '../state/ducks/calling';
import { usePageVisibility } from '../util/hooks';
import { nonRenderedRemoteParticipant } from '../util/ringrtc/nonRenderedRemoteParticipant';
// This value should be kept in sync with the hard-coded CSS height.
const PIP_VIDEO_HEIGHT_PX = 120;
const NoVideo = ({
activeCall,
@ -57,6 +63,7 @@ export interface PropsType {
activeCall: ActiveCallType;
getGroupCallVideoFrameSource: (demuxId: number) => VideoFrameSource;
i18n: LocalizerType;
setGroupCallVideoRequest: (_: Array<GroupCallVideoRequest>) => void;
setRendererCanvas: (_: SetRendererCanvasType) => void;
}
@ -64,10 +71,13 @@ export const CallingPipRemoteVideo = ({
activeCall,
getGroupCallVideoFrameSource,
i18n,
setGroupCallVideoRequest,
setRendererCanvas,
}: PropsType): JSX.Element => {
const { call, conversation, groupCallParticipants } = activeCall;
const isPageVisible = usePageVisibility();
const activeGroupCallSpeaker:
| undefined
| GroupCallRemoteParticipantType = useMemo(() => {
@ -81,6 +91,42 @@ export const CallingPipRemoteVideo = ({
);
}, [call.callMode, groupCallParticipants]);
useEffect(() => {
if (call.callMode !== CallMode.Group) {
return;
}
if (isPageVisible) {
setGroupCallVideoRequest(
groupCallParticipants.map(participant => {
const isVisible =
participant === activeGroupCallSpeaker &&
participant.hasRemoteVideo;
if (isVisible) {
return {
demuxId: participant.demuxId,
width: Math.floor(
PIP_VIDEO_HEIGHT_PX * participant.videoAspectRatio
),
height: PIP_VIDEO_HEIGHT_PX,
};
}
return nonRenderedRemoteParticipant(participant);
})
);
} else {
setGroupCallVideoRequest(
groupCallParticipants.map(nonRenderedRemoteParticipant)
);
}
}, [
call.callMode,
groupCallParticipants,
activeGroupCallSpeaker,
isPageVisible,
setGroupCallVideoRequest,
]);
if (call.callMode === CallMode.Direct) {
if (!call.hasRemoteVideo) {
return <NoVideo activeCall={activeCall} i18n={i18n} />;

View file

@ -1,19 +1,25 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useState, useMemo } from 'react';
import React, { useState, useMemo, useEffect } from 'react';
import Measure from 'react-measure';
import { takeWhile, chunk, maxBy, flatten } from 'lodash';
import { GroupCallRemoteParticipant } from './GroupCallRemoteParticipant';
import {
GroupCallRemoteParticipantType,
GroupCallVideoRequest,
VideoFrameSource,
} from '../types/Calling';
import { LocalizerType } from '../types/Util';
import { usePageVisibility } from '../util/hooks';
import { nonRenderedRemoteParticipant } from '../util/ringrtc/nonRenderedRemoteParticipant';
const MIN_RENDERED_HEIGHT = 10;
const PARTICIPANT_MARGIN = 10;
// We scale our video requests down for performance. This number is somewhat arbitrary.
const VIDEO_REQUEST_SCALAR = 0.75;
interface Dimensions {
width: number;
height: number;
@ -28,6 +34,7 @@ interface PropsType {
getGroupCallVideoFrameSource: (demuxId: number) => VideoFrameSource;
i18n: LocalizerType;
remoteParticipants: ReadonlyArray<GroupCallRemoteParticipantType>;
setGroupCallVideoRequest: (_: Array<GroupCallVideoRequest>) => void;
}
// This component lays out group call remote participants. It uses a custom layout
@ -58,11 +65,13 @@ export const GroupCallRemoteParticipants: React.FC<PropsType> = ({
getGroupCallVideoFrameSource,
i18n,
remoteParticipants,
setGroupCallVideoRequest,
}) => {
const [containerDimensions, setContainerDimensions] = useState<Dimensions>({
width: 0,
height: 0,
});
const isPageVisible = usePageVisibility();
// 1. Figure out the maximum number of possible rows that could fit on the screen.
//
@ -101,6 +110,9 @@ export const GroupCallRemoteParticipants: React.FC<PropsType> = ({
return totalWidth < maxTotalWidth;
});
}, [maxRowCount, containerDimensions.width, remoteParticipants]);
const overflowedParticipants: Array<GroupCallRemoteParticipantType> = remoteParticipants.slice(
visibleParticipants.length
);
// 3. For each possible number of rows (starting at 0 and ending at `maxRowCount`),
// distribute participants across the rows at the minimum height. Then find the
@ -216,7 +228,39 @@ export const GroupCallRemoteParticipants: React.FC<PropsType> = ({
});
}
);
const remoteParticipantElements = flatten(rowElements);
useEffect(() => {
if (isPageVisible) {
setGroupCallVideoRequest([
...visibleParticipants.map(participant => {
if (participant.hasRemoteVideo) {
return {
demuxId: participant.demuxId,
width: Math.floor(
gridParticipantHeight *
participant.videoAspectRatio *
VIDEO_REQUEST_SCALAR
),
height: Math.floor(gridParticipantHeight * VIDEO_REQUEST_SCALAR),
};
}
return nonRenderedRemoteParticipant(participant);
}),
...overflowedParticipants.map(nonRenderedRemoteParticipant),
]);
} else {
setGroupCallVideoRequest(
remoteParticipants.map(nonRenderedRemoteParticipant)
);
}
}, [
gridParticipantHeight,
isPageVisible,
overflowedParticipants,
remoteParticipants,
setGroupCallVideoRequest,
visibleParticipants,
]);
return (
<Measure
@ -231,7 +275,7 @@ export const GroupCallRemoteParticipants: React.FC<PropsType> = ({
>
{({ measureRef }) => (
<div className="module-ongoing-call__grid" ref={measureRef}>
{remoteParticipantElements}
{flatten(rowElements)}
</div>
)}
</Measure>

View file

@ -27,6 +27,7 @@ import {
RingRTC,
UserId,
VideoFrameSource,
VideoRequest,
} from 'ringrtc';
import { uniqBy, noop } from 'lodash';
@ -526,6 +527,13 @@ export class CallingClass {
return this.getDirectCall(conversationId)?.callId;
}
public setGroupCallVideoRequest(
conversationId: string,
resolutions: Array<VideoRequest>
): void {
this.getGroupCall(conversationId)?.requestVideo(resolutions);
}
// See the comment in types/Calling.ts to explain why we have to do this conversion.
private convertRingRtcConnectionState(
connectionState: ConnectionState

View file

@ -19,6 +19,7 @@ import {
GroupCallJoinState,
GroupCallPeekedParticipantType,
GroupCallRemoteParticipantType,
GroupCallVideoRequest,
MediaDeviceSettings,
} from '../../types/Calling';
import { callingTones } from '../../util/callingTones';
@ -168,6 +169,11 @@ export type SetLocalVideoType = {
enabled: boolean;
};
export type SetGroupCallVideoRequestType = {
conversationId: string;
resolutions: Array<GroupCallVideoRequest>;
};
export type ShowCallLobbyType =
| {
callMode: CallMode.Direct;
@ -684,6 +690,22 @@ function setLocalVideo(
};
}
function setGroupCallVideoRequest(
payload: SetGroupCallVideoRequestType
): ThunkAction<void, RootStateType, unknown, never> {
return () => {
calling.setGroupCallVideoRequest(
payload.conversationId,
payload.resolutions.map(resolution => ({
...resolution,
// The `framerate` property in RingRTC has to be set, even if it's set to
// `undefined`.
framerate: undefined,
}))
);
};
}
function showCallLobby(payload: ShowCallLobbyType): CallLobbyActionType {
return {
type: SHOW_CALL_LOBBY,
@ -758,6 +780,7 @@ export const actions = {
setRendererCanvas,
setLocalAudio,
setLocalVideo,
setGroupCallVideoRequest,
showCallLobby,
startCall,
toggleParticipants,

View file

@ -0,0 +1,16 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import { nonRenderedRemoteParticipant } from '../../../util/ringrtc/nonRenderedRemoteParticipant';
describe('nonRenderedRemoteParticipant', () => {
it('returns a video request object a width and height of 0', () => {
assert.deepEqual(nonRenderedRemoteParticipant({ demuxId: 123 }), {
demuxId: 123,
width: 0,
height: 0,
});
});
});

View file

@ -84,6 +84,13 @@ export interface GroupCallRemoteParticipantType {
videoAspectRatio: number;
}
// Similar to RingRTC's `VideoRequest` but without the `framerate` property.
export interface GroupCallVideoRequest {
demuxId: number;
width: number;
height: number;
}
// Should match RingRTC's VideoFrameSource
export interface VideoFrameSource {
receiveVideoFrame(buffer: ArrayBuffer): [number, number] | undefined;

View file

@ -44,3 +44,25 @@ export const useBoundActions = <T extends ActionCreatorsMapObject>(
return bindActionCreators(actions, dispatch);
}, [actions, dispatch]);
};
export const usePageVisibility = (): boolean => {
const [result, setResult] = React.useState(!document.hidden);
React.useEffect(() => {
const onVisibilityChange = () => {
setResult(!document.hidden);
};
document.addEventListener('visibilitychange', onVisibilityChange, false);
return () => {
document.removeEventListener(
'visibilitychange',
onVisibilityChange,
false
);
};
}, []);
return result;
};

View file

@ -14427,7 +14427,7 @@
"rule": "React-useRef",
"path": "ts/components/CallingPip.tsx",
"line": " const localVideoRef = React.useRef(null);",
"lineNumber": 78,
"lineNumber": 80,
"reasonCategory": "usageTrusted",
"updated": "2020-10-26T19:12:24.410Z",
"reasonDetail": "Used to get the local video element for rendering."

View file

@ -0,0 +1,12 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { GroupCallVideoRequest } from '../../types/Calling';
export const nonRenderedRemoteParticipant = ({
demuxId,
}: Readonly<{ demuxId: number }>): GroupCallVideoRequest => ({
demuxId,
width: 0,
height: 0,
});