Share group calling frame buffers to reduce memory usage
This commit is contained in:
parent
4c40d861cf
commit
8e1391c70c
7 changed files with 71 additions and 34 deletions
|
@ -1,6 +1,9 @@
|
||||||
// Copyright 2020 Signal Messenger, LLC
|
// Copyright 2020-2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
export const REQUESTED_VIDEO_WIDTH = 640;
|
export const REQUESTED_VIDEO_WIDTH = 640;
|
||||||
export const REQUESTED_VIDEO_HEIGHT = 480;
|
export const REQUESTED_VIDEO_HEIGHT = 480;
|
||||||
export const REQUESTED_VIDEO_FRAMERATE = 30;
|
export const REQUESTED_VIDEO_FRAMERATE = 30;
|
||||||
|
|
||||||
|
export const MAX_FRAME_SIZE = 1920 * 1080;
|
||||||
|
export const FRAME_BUFFER_SIZE = MAX_FRAME_SIZE * 4;
|
||||||
|
|
24
ts/calling/useGetCallingFrameBuffer.ts
Normal file
24
ts/calling/useGetCallingFrameBuffer.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { useRef, useCallback } from 'react';
|
||||||
|
import { FRAME_BUFFER_SIZE } from './constants';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A hook that returns a function. This function returns a "singleton" `ArrayBuffer` to be
|
||||||
|
* used in call video rendering.
|
||||||
|
*
|
||||||
|
* This is most useful for group calls, where we can reuse the same frame buffer instead
|
||||||
|
* of allocating one per participant. Be careful when using this buffer elsewhere, as it
|
||||||
|
* is not cleaned up and may hold stale data.
|
||||||
|
*/
|
||||||
|
export function useGetCallingFrameBuffer(): () => ArrayBuffer {
|
||||||
|
const ref = useRef<ArrayBuffer | null>(null);
|
||||||
|
|
||||||
|
return useCallback(() => {
|
||||||
|
if (!ref.current) {
|
||||||
|
ref.current = new ArrayBuffer(FRAME_BUFFER_SIZE);
|
||||||
|
}
|
||||||
|
return ref.current;
|
||||||
|
}, []);
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2020 Signal Messenger, LLC
|
// Copyright 2020-2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import React, { useMemo, useEffect } from 'react';
|
import React, { useMemo, useEffect } from 'react';
|
||||||
|
@ -16,6 +16,7 @@ import {
|
||||||
VideoFrameSource,
|
VideoFrameSource,
|
||||||
} from '../types/Calling';
|
} from '../types/Calling';
|
||||||
import { SetRendererCanvasType } from '../state/ducks/calling';
|
import { SetRendererCanvasType } from '../state/ducks/calling';
|
||||||
|
import { useGetCallingFrameBuffer } from '../calling/useGetCallingFrameBuffer';
|
||||||
import { usePageVisibility } from '../util/hooks';
|
import { usePageVisibility } from '../util/hooks';
|
||||||
import { nonRenderedRemoteParticipant } from '../util/ringrtc/nonRenderedRemoteParticipant';
|
import { nonRenderedRemoteParticipant } from '../util/ringrtc/nonRenderedRemoteParticipant';
|
||||||
|
|
||||||
|
@ -77,6 +78,8 @@ export const CallingPipRemoteVideo = ({
|
||||||
}: PropsType): JSX.Element => {
|
}: PropsType): JSX.Element => {
|
||||||
const { conversation } = activeCall;
|
const { conversation } = activeCall;
|
||||||
|
|
||||||
|
const getGroupCallFrameBuffer = useGetCallingFrameBuffer();
|
||||||
|
|
||||||
const isPageVisible = usePageVisibility();
|
const isPageVisible = usePageVisibility();
|
||||||
|
|
||||||
const activeGroupCallSpeaker:
|
const activeGroupCallSpeaker:
|
||||||
|
@ -155,6 +158,7 @@ export const CallingPipRemoteVideo = ({
|
||||||
return (
|
return (
|
||||||
<div className="module-calling-pip__video--remote">
|
<div className="module-calling-pip__video--remote">
|
||||||
<GroupCallRemoteParticipant
|
<GroupCallRemoteParticipant
|
||||||
|
getFrameBuffer={getGroupCallFrameBuffer}
|
||||||
getGroupCallVideoFrameSource={getGroupCallVideoFrameSource}
|
getGroupCallVideoFrameSource={getGroupCallVideoFrameSource}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
isInPip
|
isInPip
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
// Copyright 2020 Signal Messenger, LLC
|
// Copyright 2020-2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { noop } from 'lodash';
|
import { memoize, noop } from 'lodash';
|
||||||
import { storiesOf } from '@storybook/react';
|
import { storiesOf } from '@storybook/react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -10,6 +10,7 @@ import {
|
||||||
PropsType,
|
PropsType,
|
||||||
} from './GroupCallRemoteParticipant';
|
} from './GroupCallRemoteParticipant';
|
||||||
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
|
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
|
||||||
|
import { FRAME_BUFFER_SIZE } from '../calling/constants';
|
||||||
import { setup as setupI18n } from '../../js/modules/i18n';
|
import { setup as setupI18n } from '../../js/modules/i18n';
|
||||||
import enMessages from '../../_locales/en/messages.json';
|
import enMessages from '../../_locales/en/messages.json';
|
||||||
|
|
||||||
|
@ -27,10 +28,13 @@ type OverridePropsType =
|
||||||
width: number;
|
width: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getFrameBuffer = memoize(() => new ArrayBuffer(FRAME_BUFFER_SIZE));
|
||||||
|
|
||||||
const createProps = (
|
const createProps = (
|
||||||
overrideProps: OverridePropsType,
|
overrideProps: OverridePropsType,
|
||||||
isBlocked?: boolean
|
isBlocked?: boolean
|
||||||
): PropsType => ({
|
): PropsType => ({
|
||||||
|
getFrameBuffer,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
getGroupCallVideoFrameSource: noop as any,
|
getGroupCallVideoFrameSource: noop as any,
|
||||||
i18n,
|
i18n,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2020 Signal Messenger, LLC
|
// Copyright 2020-2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import React, {
|
import React, {
|
||||||
|
@ -21,11 +21,10 @@ import { Avatar, AvatarSize } from './Avatar';
|
||||||
import { ConfirmationModal } from './ConfirmationModal';
|
import { ConfirmationModal } from './ConfirmationModal';
|
||||||
import { Intl } from './Intl';
|
import { Intl } from './Intl';
|
||||||
import { ContactName } from './conversation/ContactName';
|
import { ContactName } from './conversation/ContactName';
|
||||||
|
import { MAX_FRAME_SIZE } from '../calling/constants';
|
||||||
// The max size video frame we'll support (in RGBA)
|
|
||||||
const FRAME_BUFFER_SIZE = 1920 * 1080 * 4;
|
|
||||||
|
|
||||||
interface BasePropsType {
|
interface BasePropsType {
|
||||||
|
getFrameBuffer: () => ArrayBuffer;
|
||||||
getGroupCallVideoFrameSource: (demuxId: number) => VideoFrameSource;
|
getGroupCallVideoFrameSource: (demuxId: number) => VideoFrameSource;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
remoteParticipant: GroupCallRemoteParticipantType;
|
remoteParticipant: GroupCallRemoteParticipantType;
|
||||||
|
@ -47,7 +46,7 @@ export type PropsType = BasePropsType & (InPipPropsType | NotInPipPropsType);
|
||||||
|
|
||||||
export const GroupCallRemoteParticipant: React.FC<PropsType> = React.memo(
|
export const GroupCallRemoteParticipant: React.FC<PropsType> = React.memo(
|
||||||
props => {
|
props => {
|
||||||
const { getGroupCallVideoFrameSource, i18n } = props;
|
const { getFrameBuffer, getGroupCallVideoFrameSource, i18n } = props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
avatarPath,
|
avatarPath,
|
||||||
|
@ -66,9 +65,6 @@ export const GroupCallRemoteParticipant: React.FC<PropsType> = React.memo(
|
||||||
|
|
||||||
const remoteVideoRef = useRef<HTMLCanvasElement | null>(null);
|
const remoteVideoRef = useRef<HTMLCanvasElement | null>(null);
|
||||||
const canvasContextRef = useRef<CanvasRenderingContext2D | null>(null);
|
const canvasContextRef = useRef<CanvasRenderingContext2D | null>(null);
|
||||||
const frameBufferRef = useRef<ArrayBuffer>(
|
|
||||||
new ArrayBuffer(FRAME_BUFFER_SIZE)
|
|
||||||
);
|
|
||||||
|
|
||||||
const videoFrameSource = useMemo(
|
const videoFrameSource = useMemo(
|
||||||
() => getGroupCallVideoFrameSource(demuxId),
|
() => getGroupCallVideoFrameSource(demuxId),
|
||||||
|
@ -86,15 +82,22 @@ export const GroupCallRemoteParticipant: React.FC<PropsType> = React.memo(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const frameDimensions = videoFrameSource.receiveVideoFrame(
|
// This frame buffer is shared by all participants, so it may contain pixel data
|
||||||
frameBufferRef.current
|
// for other participants, or pixel data from a previous frame. That's why we
|
||||||
);
|
// return early and use the `frameWidth` and `frameHeight`.
|
||||||
|
const frameBuffer = getFrameBuffer();
|
||||||
|
const frameDimensions = videoFrameSource.receiveVideoFrame(frameBuffer);
|
||||||
if (!frameDimensions) {
|
if (!frameDimensions) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [frameWidth, frameHeight] = frameDimensions;
|
const [frameWidth, frameHeight] = frameDimensions;
|
||||||
if (frameWidth < 2 || frameHeight < 2) {
|
|
||||||
|
if (
|
||||||
|
frameWidth < 2 ||
|
||||||
|
frameHeight < 2 ||
|
||||||
|
frameWidth * frameHeight > MAX_FRAME_SIZE
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,11 +106,7 @@ export const GroupCallRemoteParticipant: React.FC<PropsType> = React.memo(
|
||||||
|
|
||||||
canvasContext.putImageData(
|
canvasContext.putImageData(
|
||||||
new ImageData(
|
new ImageData(
|
||||||
new Uint8ClampedArray(
|
new Uint8ClampedArray(frameBuffer, 0, frameWidth * frameHeight * 4),
|
||||||
frameBufferRef.current,
|
|
||||||
0,
|
|
||||||
frameWidth * frameHeight * 4
|
|
||||||
),
|
|
||||||
frameWidth,
|
frameWidth,
|
||||||
frameHeight
|
frameHeight
|
||||||
),
|
),
|
||||||
|
@ -116,7 +115,7 @@ export const GroupCallRemoteParticipant: React.FC<PropsType> = React.memo(
|
||||||
);
|
);
|
||||||
|
|
||||||
setIsWide(frameWidth > frameHeight);
|
setIsWide(frameWidth > frameHeight);
|
||||||
}, [videoFrameSource]);
|
}, [getFrameBuffer, videoFrameSource]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!hasRemoteVideo) {
|
if (!hasRemoteVideo) {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2020 Signal Messenger, LLC
|
// Copyright 2020-2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import React, { useState, useMemo, useEffect } from 'react';
|
import React, { useState, useMemo, useEffect } from 'react';
|
||||||
|
@ -10,6 +10,7 @@ import {
|
||||||
GroupCallVideoRequest,
|
GroupCallVideoRequest,
|
||||||
VideoFrameSource,
|
VideoFrameSource,
|
||||||
} from '../types/Calling';
|
} from '../types/Calling';
|
||||||
|
import { useGetCallingFrameBuffer } from '../calling/useGetCallingFrameBuffer';
|
||||||
import { LocalizerType } from '../types/Util';
|
import { LocalizerType } from '../types/Util';
|
||||||
import { usePageVisibility } from '../util/hooks';
|
import { usePageVisibility } from '../util/hooks';
|
||||||
import { nonRenderedRemoteParticipant } from '../util/ringrtc/nonRenderedRemoteParticipant';
|
import { nonRenderedRemoteParticipant } from '../util/ringrtc/nonRenderedRemoteParticipant';
|
||||||
|
@ -72,6 +73,7 @@ export const GroupCallRemoteParticipants: React.FC<PropsType> = ({
|
||||||
height: 0,
|
height: 0,
|
||||||
});
|
});
|
||||||
const isPageVisible = usePageVisibility();
|
const isPageVisible = usePageVisibility();
|
||||||
|
const getFrameBuffer = useGetCallingFrameBuffer();
|
||||||
|
|
||||||
// 1. Figure out the maximum number of possible rows that could fit on the screen.
|
// 1. Figure out the maximum number of possible rows that could fit on the screen.
|
||||||
//
|
//
|
||||||
|
@ -216,6 +218,7 @@ export const GroupCallRemoteParticipants: React.FC<PropsType> = ({
|
||||||
return (
|
return (
|
||||||
<GroupCallRemoteParticipant
|
<GroupCallRemoteParticipant
|
||||||
key={remoteParticipant.demuxId}
|
key={remoteParticipant.demuxId}
|
||||||
|
getFrameBuffer={getFrameBuffer}
|
||||||
getGroupCallVideoFrameSource={getGroupCallVideoFrameSource}
|
getGroupCallVideoFrameSource={getGroupCallVideoFrameSource}
|
||||||
height={gridParticipantHeight}
|
height={gridParticipantHeight}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
|
|
|
@ -14325,6 +14325,15 @@
|
||||||
"updated": "2018-09-17T20:50:40.689Z",
|
"updated": "2018-09-17T20:50:40.689Z",
|
||||||
"reasonDetail": "Hard-coded value"
|
"reasonDetail": "Hard-coded value"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"rule": "React-useRef",
|
||||||
|
"path": "ts/calling/useGetCallingFrameBuffer.js",
|
||||||
|
"line": " const ref = react_1.useRef(null);",
|
||||||
|
"lineNumber": 17,
|
||||||
|
"reasonCategory": "usageTrusted",
|
||||||
|
"updated": "2021-01-06T00:47:54.313Z",
|
||||||
|
"reasonDetail": "Needed to render remote video elements. Doesn't interact with the DOM."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"rule": "React-useRef",
|
"rule": "React-useRef",
|
||||||
"path": "ts/components/AvatarPopup.js",
|
"path": "ts/components/AvatarPopup.js",
|
||||||
|
@ -14518,7 +14527,7 @@
|
||||||
"rule": "React-useRef",
|
"rule": "React-useRef",
|
||||||
"path": "ts/components/GroupCallRemoteParticipant.js",
|
"path": "ts/components/GroupCallRemoteParticipant.js",
|
||||||
"line": " const remoteVideoRef = react_1.useRef(null);",
|
"line": " const remoteVideoRef = react_1.useRef(null);",
|
||||||
"lineNumber": 44,
|
"lineNumber": 43,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2020-11-11T21:56:04.179Z",
|
"updated": "2020-11-11T21:56:04.179Z",
|
||||||
"reasonDetail": "Needed to render the remote video element."
|
"reasonDetail": "Needed to render the remote video element."
|
||||||
|
@ -14527,20 +14536,11 @@
|
||||||
"rule": "React-useRef",
|
"rule": "React-useRef",
|
||||||
"path": "ts/components/GroupCallRemoteParticipant.js",
|
"path": "ts/components/GroupCallRemoteParticipant.js",
|
||||||
"line": " const canvasContextRef = react_1.useRef(null);",
|
"line": " const canvasContextRef = react_1.useRef(null);",
|
||||||
"lineNumber": 45,
|
"lineNumber": 44,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2020-11-17T23:29:38.698Z",
|
"updated": "2020-11-17T23:29:38.698Z",
|
||||||
"reasonDetail": "Doesn't touch the DOM."
|
"reasonDetail": "Doesn't touch the DOM."
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"rule": "React-useRef",
|
|
||||||
"path": "ts/components/GroupCallRemoteParticipant.js",
|
|
||||||
"line": " const frameBufferRef = react_1.useRef(new ArrayBuffer(FRAME_BUFFER_SIZE));",
|
|
||||||
"lineNumber": 46,
|
|
||||||
"reasonCategory": "usageTrusted",
|
|
||||||
"updated": "2020-11-17T16:24:25.480Z",
|
|
||||||
"reasonDetail": "Doesn't touch the DOM."
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "ts/components/Intl.js",
|
"path": "ts/components/Intl.js",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue