2020-11-13 19:57:55 +00:00
|
|
|
// Copyright 2020 Signal Messenger, LLC
|
|
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
|
2020-11-17 19:49:48 +00:00
|
|
|
import React, {
|
|
|
|
useState,
|
|
|
|
useRef,
|
|
|
|
useMemo,
|
|
|
|
useCallback,
|
|
|
|
useEffect,
|
|
|
|
CSSProperties,
|
|
|
|
} from 'react';
|
2020-11-16 19:57:58 +00:00
|
|
|
import classNames from 'classnames';
|
2020-11-13 19:57:55 +00:00
|
|
|
import { noop } from 'lodash';
|
2020-11-19 18:13:36 +00:00
|
|
|
import {
|
|
|
|
GroupCallRemoteParticipantType,
|
|
|
|
VideoFrameSource,
|
|
|
|
} from '../types/Calling';
|
|
|
|
import { LocalizerType } from '../types/Util';
|
2020-11-13 19:57:55 +00:00
|
|
|
import { CallBackgroundBlur } from './CallBackgroundBlur';
|
2020-11-19 18:13:36 +00:00
|
|
|
import { Avatar, AvatarSize } from './Avatar';
|
2020-12-02 01:30:25 +00:00
|
|
|
import { ConfirmationModal } from './ConfirmationModal';
|
|
|
|
import { Intl } from './Intl';
|
2020-11-19 23:31:04 +00:00
|
|
|
import { ContactName } from './conversation/ContactName';
|
2020-11-13 19:57:55 +00:00
|
|
|
|
2020-11-17 19:49:48 +00:00
|
|
|
// The max size video frame we'll support (in RGBA)
|
|
|
|
const FRAME_BUFFER_SIZE = 1920 * 1080 * 4;
|
|
|
|
|
|
|
|
interface BasePropsType {
|
2020-11-13 19:57:55 +00:00
|
|
|
getGroupCallVideoFrameSource: (demuxId: number) => VideoFrameSource;
|
2020-11-19 18:13:36 +00:00
|
|
|
i18n: LocalizerType;
|
|
|
|
remoteParticipant: GroupCallRemoteParticipantType;
|
2020-11-17 19:49:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
interface InPipPropsType {
|
|
|
|
isInPip: true;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface NotInPipPropsType {
|
|
|
|
height: number;
|
2020-11-19 18:13:36 +00:00
|
|
|
isInPip?: false;
|
2020-11-13 19:57:55 +00:00
|
|
|
left: number;
|
|
|
|
top: number;
|
2020-11-19 18:13:36 +00:00
|
|
|
width: number;
|
2020-11-13 19:57:55 +00:00
|
|
|
}
|
|
|
|
|
2020-11-19 23:31:04 +00:00
|
|
|
export type PropsType = BasePropsType & (InPipPropsType | NotInPipPropsType);
|
2020-11-17 19:49:48 +00:00
|
|
|
|
2020-11-18 19:07:25 +00:00
|
|
|
export const GroupCallRemoteParticipant: React.FC<PropsType> = React.memo(
|
|
|
|
props => {
|
2020-12-02 01:30:25 +00:00
|
|
|
const { getGroupCallVideoFrameSource, i18n } = props;
|
2020-11-19 18:13:36 +00:00
|
|
|
|
2020-11-18 19:07:25 +00:00
|
|
|
const {
|
2020-11-19 18:13:36 +00:00
|
|
|
avatarPath,
|
|
|
|
color,
|
2020-11-18 19:07:25 +00:00
|
|
|
demuxId,
|
|
|
|
hasRemoteAudio,
|
|
|
|
hasRemoteVideo,
|
2020-12-02 01:30:25 +00:00
|
|
|
isBlocked,
|
|
|
|
profileName,
|
|
|
|
title,
|
2020-11-19 18:13:36 +00:00
|
|
|
} = props.remoteParticipant;
|
2020-11-18 19:07:25 +00:00
|
|
|
|
|
|
|
const [isWide, setIsWide] = useState(true);
|
2020-11-19 23:31:04 +00:00
|
|
|
const [hasHover, setHover] = useState(false);
|
2020-12-02 01:30:25 +00:00
|
|
|
const [showBlockInfo, setShowBlockInfo] = useState(false);
|
2020-11-18 19:07:25 +00:00
|
|
|
|
|
|
|
const remoteVideoRef = useRef<HTMLCanvasElement | null>(null);
|
|
|
|
const canvasContextRef = useRef<CanvasRenderingContext2D | null>(null);
|
|
|
|
const frameBufferRef = useRef<ArrayBuffer>(
|
|
|
|
new ArrayBuffer(FRAME_BUFFER_SIZE)
|
|
|
|
);
|
2020-11-13 19:57:55 +00:00
|
|
|
|
2020-11-18 19:07:25 +00:00
|
|
|
const videoFrameSource = useMemo(
|
|
|
|
() => getGroupCallVideoFrameSource(demuxId),
|
|
|
|
[getGroupCallVideoFrameSource, demuxId]
|
2020-11-17 19:49:48 +00:00
|
|
|
);
|
|
|
|
|
2020-11-18 19:07:25 +00:00
|
|
|
const renderVideoFrame = useCallback(() => {
|
|
|
|
const canvasEl = remoteVideoRef.current;
|
|
|
|
if (!canvasEl) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const canvasContext = canvasContextRef.current;
|
|
|
|
if (!canvasContext) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const frameDimensions = videoFrameSource.receiveVideoFrame(
|
|
|
|
frameBufferRef.current
|
|
|
|
);
|
|
|
|
if (!frameDimensions) {
|
|
|
|
return;
|
|
|
|
}
|
2020-11-17 19:49:48 +00:00
|
|
|
|
2020-11-18 19:07:25 +00:00
|
|
|
const [frameWidth, frameHeight] = frameDimensions;
|
|
|
|
if (frameWidth < 2 || frameHeight < 2) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
canvasEl.width = frameWidth;
|
|
|
|
canvasEl.height = frameHeight;
|
|
|
|
|
|
|
|
canvasContext.putImageData(
|
|
|
|
new ImageData(
|
|
|
|
new Uint8ClampedArray(
|
|
|
|
frameBufferRef.current,
|
|
|
|
0,
|
|
|
|
frameWidth * frameHeight * 4
|
|
|
|
),
|
|
|
|
frameWidth,
|
|
|
|
frameHeight
|
2020-11-17 19:49:48 +00:00
|
|
|
),
|
2020-11-18 19:07:25 +00:00
|
|
|
0,
|
|
|
|
0
|
|
|
|
);
|
|
|
|
|
|
|
|
setIsWide(frameWidth > frameHeight);
|
|
|
|
}, [videoFrameSource]);
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (!hasRemoteVideo) {
|
|
|
|
return noop;
|
|
|
|
}
|
|
|
|
|
|
|
|
let rafId = requestAnimationFrame(tick);
|
|
|
|
|
|
|
|
function tick() {
|
|
|
|
renderVideoFrame();
|
|
|
|
rafId = requestAnimationFrame(tick);
|
|
|
|
}
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
cancelAnimationFrame(rafId);
|
|
|
|
};
|
|
|
|
}, [hasRemoteVideo, renderVideoFrame, videoFrameSource]);
|
|
|
|
|
|
|
|
let canvasStyles: CSSProperties;
|
|
|
|
let containerStyles: CSSProperties;
|
2020-11-17 19:49:48 +00:00
|
|
|
|
|
|
|
// If our `width` and `height` props don't match the canvas's aspect ratio, we want to
|
|
|
|
// fill the container. This can happen when RingRTC gives us an inaccurate
|
|
|
|
// `videoAspectRatio`, or if the container is an unexpected size.
|
2020-11-18 19:07:25 +00:00
|
|
|
if (isWide) {
|
|
|
|
canvasStyles = { width: '100%' };
|
2020-11-13 19:57:55 +00:00
|
|
|
} else {
|
2020-11-18 19:07:25 +00:00
|
|
|
canvasStyles = { height: '100%' };
|
2020-11-17 19:49:48 +00:00
|
|
|
}
|
|
|
|
|
2020-11-19 18:13:36 +00:00
|
|
|
let avatarSize: number;
|
|
|
|
|
2020-11-18 19:07:25 +00:00
|
|
|
// TypeScript isn't smart enough to know that `isInPip` by itself disambiguates the
|
|
|
|
// types, so we have to use `props.isInPip` instead.
|
|
|
|
// eslint-disable-next-line react/destructuring-assignment
|
|
|
|
if (props.isInPip) {
|
|
|
|
containerStyles = canvasStyles;
|
2020-11-19 18:13:36 +00:00
|
|
|
avatarSize = AvatarSize.FIFTY_TWO;
|
2020-11-18 19:07:25 +00:00
|
|
|
} else {
|
|
|
|
const { top, left, width, height } = props;
|
2020-11-19 18:13:36 +00:00
|
|
|
const shorterDimension = Math.min(width, height);
|
|
|
|
|
|
|
|
if (shorterDimension >= 240) {
|
|
|
|
avatarSize = AvatarSize.ONE_HUNDRED_TWELVE;
|
|
|
|
} else if (shorterDimension >= 180) {
|
|
|
|
avatarSize = AvatarSize.EIGHTY;
|
|
|
|
} else {
|
|
|
|
avatarSize = AvatarSize.FIFTY_TWO;
|
|
|
|
}
|
2020-11-18 19:07:25 +00:00
|
|
|
|
|
|
|
containerStyles = {
|
|
|
|
height,
|
|
|
|
left,
|
|
|
|
position: 'absolute',
|
|
|
|
top,
|
|
|
|
width,
|
|
|
|
};
|
2020-11-13 19:57:55 +00:00
|
|
|
}
|
2020-11-17 19:49:48 +00:00
|
|
|
|
2020-11-20 20:24:55 +00:00
|
|
|
const showHover = hasHover && !props.isInPip;
|
2020-12-02 01:30:25 +00:00
|
|
|
const canShowVideo = hasRemoteVideo && !isBlocked;
|
|
|
|
|
|
|
|
if (showBlockInfo) {
|
|
|
|
return (
|
|
|
|
<ConfirmationModal
|
|
|
|
i18n={i18n}
|
|
|
|
onClose={() => {
|
|
|
|
setShowBlockInfo(false);
|
|
|
|
}}
|
|
|
|
title={
|
|
|
|
<div className="module-ongoing-call__group-call-remote-participant__blocked--modal-title">
|
|
|
|
<Intl
|
|
|
|
i18n={i18n}
|
|
|
|
id="calling__you-have-blocked"
|
|
|
|
components={[
|
|
|
|
<ContactName
|
|
|
|
key="name"
|
|
|
|
profileName={profileName}
|
|
|
|
title={title}
|
|
|
|
i18n={i18n}
|
|
|
|
/>,
|
|
|
|
]}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
}
|
|
|
|
actions={[
|
|
|
|
{
|
|
|
|
text: i18n('ok'),
|
|
|
|
action: () => {
|
|
|
|
setShowBlockInfo(false);
|
|
|
|
},
|
|
|
|
style: 'affirmative',
|
|
|
|
},
|
|
|
|
]}
|
|
|
|
>
|
|
|
|
{i18n('calling__block-info')}
|
|
|
|
</ConfirmationModal>
|
|
|
|
);
|
|
|
|
}
|
2020-11-20 20:24:55 +00:00
|
|
|
|
2020-11-18 19:07:25 +00:00
|
|
|
return (
|
|
|
|
<div
|
2020-11-19 23:31:04 +00:00
|
|
|
className="module-ongoing-call__group-call-remote-participant"
|
|
|
|
onMouseEnter={() => setHover(true)}
|
|
|
|
onMouseLeave={() => setHover(false)}
|
2020-11-18 19:07:25 +00:00
|
|
|
style={containerStyles}
|
|
|
|
>
|
2020-11-20 20:24:55 +00:00
|
|
|
{showHover && (
|
2020-11-19 23:31:04 +00:00
|
|
|
<div
|
|
|
|
className={classNames(
|
|
|
|
'module-ongoing-call__group-call-remote-participant--title',
|
|
|
|
{
|
|
|
|
'module-ongoing-call__group-call-remote-participant--audio-muted': !hasRemoteAudio,
|
|
|
|
}
|
|
|
|
)}
|
|
|
|
>
|
|
|
|
<ContactName
|
|
|
|
module="module-ongoing-call__group-call-remote-participant--contact-name"
|
|
|
|
profileName={profileName}
|
|
|
|
title={title}
|
2020-12-02 01:30:25 +00:00
|
|
|
i18n={i18n}
|
2020-11-19 23:31:04 +00:00
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
)}
|
2020-12-02 01:30:25 +00:00
|
|
|
{canShowVideo ? (
|
2020-11-18 19:07:25 +00:00
|
|
|
<canvas
|
|
|
|
className="module-ongoing-call__group-call-remote-participant__remote-video"
|
|
|
|
style={canvasStyles}
|
|
|
|
ref={canvasEl => {
|
|
|
|
remoteVideoRef.current = canvasEl;
|
|
|
|
if (canvasEl) {
|
|
|
|
canvasContextRef.current = canvasEl.getContext('2d', {
|
|
|
|
alpha: false,
|
|
|
|
desynchronized: true,
|
|
|
|
storage: 'discardable',
|
|
|
|
} as CanvasRenderingContext2DSettings);
|
|
|
|
} else {
|
|
|
|
canvasContextRef.current = null;
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
) : (
|
2020-11-19 18:13:36 +00:00
|
|
|
<CallBackgroundBlur avatarPath={avatarPath} color={color}>
|
2020-12-02 01:30:25 +00:00
|
|
|
{isBlocked ? (
|
|
|
|
<>
|
|
|
|
<i className="module-ongoing-call__group-call-remote-participant__blocked" />
|
|
|
|
<button
|
|
|
|
type="button"
|
|
|
|
className="module-ongoing-call__group-call-remote-participant__blocked--info"
|
|
|
|
onClick={() => {
|
|
|
|
setShowBlockInfo(true);
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
{i18n('moreInfo')}
|
|
|
|
</button>
|
|
|
|
</>
|
|
|
|
) : (
|
|
|
|
<Avatar
|
|
|
|
avatarPath={avatarPath}
|
|
|
|
color={color || 'ultramarine'}
|
|
|
|
noteToSelf={false}
|
|
|
|
conversationType="direct"
|
|
|
|
i18n={i18n}
|
|
|
|
profileName={profileName}
|
|
|
|
title={title}
|
|
|
|
size={avatarSize}
|
|
|
|
/>
|
|
|
|
)}
|
2020-11-18 19:07:25 +00:00
|
|
|
</CallBackgroundBlur>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
);
|
2020-11-13 19:57:55 +00:00
|
|
|
}
|
2020-11-18 19:07:25 +00:00
|
|
|
);
|