Signal-Desktop/ts/components/CallingPipRemoteVideo.tsx

268 lines
8.2 KiB
TypeScript

// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useEffect } from 'react';
import { clamp, isNumber, maxBy } from 'lodash';
import type { VideoFrameSource } from '@signalapp/ringrtc';
import { Avatar, AvatarSize } from './Avatar';
import { CallBackgroundBlur } from './CallBackgroundBlur';
import { DirectCallRemoteParticipant } from './DirectCallRemoteParticipant';
import { GroupCallRemoteParticipant } from './GroupCallRemoteParticipant';
import type { LocalizerType } from '../types/Util';
import {
GroupCallJoinState,
type ActiveCallType,
type GroupCallRemoteParticipantType,
type GroupCallVideoRequest,
} from '../types/Calling';
import { CallMode } from '../types/CallDisposition';
import { AvatarColors } from '../types/Colors';
import type { SetRendererCanvasType } from '../state/ducks/calling';
import { useGetCallingFrameBuffer } from '../calling/useGetCallingFrameBuffer';
import { MAX_FRAME_HEIGHT } from '../calling/constants';
import { usePageVisibility } from '../hooks/usePageVisibility';
import { missingCaseError } from '../util/missingCaseError';
import { nonRenderedRemoteParticipant } from '../util/ringrtc/nonRenderedRemoteParticipant';
import { isReconnecting } from '../util/callingIsReconnecting';
import { isGroupOrAdhocActiveCall } from '../util/isGroupOrAdhocCall';
import { assertDev } from '../util/assert';
import type { CallingImageDataCache } from './CallManager';
import {
PIP_MAXIMUM_HEIGHT_MULTIPLIER,
PIP_MINIMUM_HEIGHT_MULTIPLIER,
PIP_WIDTH_NORMAL,
} from './CallingPip';
function BlurredBackground({
activeCall,
activeGroupCallSpeaker,
avatarSize,
darken,
i18n,
}: {
activeCall: ActiveCallType;
activeGroupCallSpeaker?: undefined | GroupCallRemoteParticipantType;
avatarSize: AvatarSize;
darken?: boolean;
i18n: LocalizerType;
}): JSX.Element {
const {
avatarPlaceholderGradient,
color,
type: conversationType,
phoneNumber,
profileName,
sharedGroupNames,
title,
} = activeCall.conversation;
const avatarUrl =
activeGroupCallSpeaker?.avatarUrl ?? activeCall.conversation.avatarUrl;
return (
<CallBackgroundBlur avatarUrl={avatarUrl} darken={darken}>
<div className="module-calling-pip__video--avatar">
<Avatar
avatarPlaceholderGradient={avatarPlaceholderGradient}
avatarUrl={avatarUrl}
badge={undefined}
color={color || AvatarColors[0]}
noteToSelf={false}
conversationType={conversationType}
i18n={i18n}
phoneNumber={phoneNumber}
profileName={profileName}
title={title}
size={avatarSize}
sharedGroupNames={sharedGroupNames}
/>
</div>
</CallBackgroundBlur>
);
}
export type PropsType = {
activeCall: ActiveCallType;
getGroupCallVideoFrameSource: (demuxId: number) => VideoFrameSource;
i18n: LocalizerType;
imageDataCache: React.RefObject<CallingImageDataCache>;
setGroupCallVideoRequest: (
_: Array<GroupCallVideoRequest>,
speakerHeight: number
) => void;
setRendererCanvas: (_: SetRendererCanvasType) => void;
height: number;
width: number;
updateHeight: (newHeight: number) => void;
};
export function CallingPipRemoteVideo({
activeCall,
getGroupCallVideoFrameSource,
imageDataCache,
i18n,
setGroupCallVideoRequest,
setRendererCanvas,
height,
width,
updateHeight,
}: PropsType): JSX.Element {
const { conversation } = activeCall;
const getGroupCallFrameBuffer = useGetCallingFrameBuffer();
const isPageVisible = usePageVisibility();
const activeGroupCallSpeaker: undefined | GroupCallRemoteParticipantType =
React.useMemo(() => {
if (!isGroupOrAdhocActiveCall(activeCall)) {
return undefined;
}
if (activeCall.joinState !== GroupCallJoinState.Joined) {
return undefined;
}
return maxBy(activeCall.remoteParticipants, participant =>
participant.presenting ? Infinity : participant.speakerTime || -Infinity
);
}, [activeCall]);
useEffect(() => {
if (isGroupOrAdhocActiveCall(activeCall)) {
if (!activeGroupCallSpeaker || !activeGroupCallSpeaker.hasRemoteVideo) {
return;
}
const { videoAspectRatio } = activeGroupCallSpeaker;
if (!isNumber(videoAspectRatio)) {
return;
}
const ratio = 1 / videoAspectRatio;
const newHeight = clamp(Math.floor(width * ratio), 1, MAX_FRAME_HEIGHT);
// Update only for portrait video that fits, otherwise leave things as they are
if (
newHeight !== height &&
ratio >= PIP_MINIMUM_HEIGHT_MULTIPLIER &&
ratio <= PIP_MAXIMUM_HEIGHT_MULTIPLIER
) {
updateHeight(newHeight);
}
if (isPageVisible) {
const participants = activeCall.remoteParticipants.map(participant => {
if (participant === activeGroupCallSpeaker) {
return {
demuxId: participant.demuxId,
width,
height: newHeight,
};
}
return nonRenderedRemoteParticipant(participant);
});
setGroupCallVideoRequest(participants, newHeight);
} else {
setGroupCallVideoRequest(
activeCall.remoteParticipants.map(nonRenderedRemoteParticipant),
0
);
}
} else {
// eslint-disable-next-line no-lonely-if
if (!activeCall.hasRemoteVideo) {
// eslint-disable-next-line no-useless-return
return;
}
// TODO: DESKTOP-8537 - with direct call video stats, call updateHeight as needed
}
}, [
activeCall,
activeGroupCallSpeaker,
height,
isPageVisible,
setGroupCallVideoRequest,
updateHeight,
width,
]);
const avatarSize =
width > PIP_WIDTH_NORMAL ? AvatarSize.NINETY_SIX : AvatarSize.SIXTY_FOUR;
switch (activeCall.callMode) {
case CallMode.Direct: {
const { hasRemoteVideo } = activeCall.remoteParticipants[0];
if (!hasRemoteVideo) {
return (
<div className="module-calling-pip__video--remote">
<BlurredBackground
activeCall={activeCall}
avatarSize={avatarSize}
i18n={i18n}
/>
</div>
);
}
assertDev(
conversation.type === 'direct',
'CallingPipRemoteVideo for direct call must be associated with direct conversation'
);
// TODO: DESKTOP-8537 - when black bars go away, we need to make some CSS changes
return (
<div className="module-calling-pip__video--remote">
<BlurredBackground
activeCall={activeCall}
avatarSize={avatarSize}
darken
i18n={i18n}
/>
<DirectCallRemoteParticipant
conversation={conversation}
hasRemoteVideo={hasRemoteVideo}
i18n={i18n}
isReconnecting={isReconnecting(activeCall)}
setRendererCanvas={setRendererCanvas}
/>
</div>
);
}
case CallMode.Group:
case CallMode.Adhoc:
if (!activeGroupCallSpeaker) {
return (
<div className="module-calling-pip__video--remote">
<BlurredBackground
activeCall={activeCall}
avatarSize={avatarSize}
i18n={i18n}
/>
</div>
);
}
return (
<div className="module-calling-pip__video--remote">
<BlurredBackground
activeCall={activeCall}
activeGroupCallSpeaker={activeGroupCallSpeaker}
avatarSize={avatarSize}
darken={activeGroupCallSpeaker.hasRemoteVideo}
i18n={i18n}
/>
<GroupCallRemoteParticipant
getFrameBuffer={getGroupCallFrameBuffer}
getGroupCallVideoFrameSource={getGroupCallVideoFrameSource}
imageDataCache={imageDataCache}
i18n={i18n}
isInPip
joinedAt={activeCall.joinedAt}
remoteParticipant={activeGroupCallSpeaker}
remoteParticipantsCount={activeCall.remoteParticipants.length}
isActiveSpeakerInSpeakerView={false}
isCallReconnecting={isReconnecting(activeCall)}
/>
</div>
);
default:
throw missingCaseError(activeCall);
}
}