Calling PiP enhancements

This commit is contained in:
Scott Nonnenberg
2025-04-15 13:37:24 -07:00
committed by GitHub
parent ecb19d5a10
commit fef6706a75
4 changed files with 123 additions and 37 deletions
+29 -8
View File
@@ -3894,6 +3894,13 @@ button.module-image__border-overlay:focus {
justify-content: center;
overflow: hidden;
&--darken {
position: absolute;
height: 100%;
width: 100%;
background-color: rgba(variables.$color-gray-60, 0.6);
}
&--blur {
background-repeat: no-repeat;
background-size: cover;
@@ -4636,13 +4643,16 @@ button.module-image__border-overlay:focus {
0px 0px 8px rgba(0, 0, 0, 0.05),
0px 8px 20px rgba(0, 0, 0, 0.3);
cursor: grab;
// This is just a starting height; the component will figure out what height it should
// be, given the aspect ratio of the provided video, pinning the width.
// These both should be kept in sync with the height/width in CallingPip.tsx
// This size is just a starting place; the component will figure out what it should
// be. When resizing for incoming portrait video, the width will be kept constant. The
// width only changes with window size.
// These both should be kept in sync with the _normal_ height/width in CallingPip.tsx
height: 286px;
width: 160px;
position: fixed;
z-index: variables.$z-index-calling-pip;
overflow: hidden;
& .module-ongoing-call__group-call-remote-participant {
border-radius: 0;
@@ -4676,6 +4686,8 @@ button.module-image__border-overlay:focus {
position: absolute;
top: 8px;
inset-inline-start: 8px;
// These are the normal widths; the size will change based on whether the window
// is large, like the overall pip.
height: 54px;
width: 80px;
@@ -4717,7 +4729,7 @@ button.module-image__border-overlay:focus {
position: absolute;
bottom: 66px;
inset-inline-start: 8px;
transition: bottom 0.3s variables.$ease-out-local-preview 0.3s;
transition: bottom 0.5s variables.$ease-out-local-preview;
&--no-controls {
bottom: 8px;
@@ -4760,16 +4772,18 @@ button.module-image__border-overlay:focus {
&__actions {
position: absolute;
bottom: 4px;
// It starts offscreen then animates in/out as it fades in/outs
bottom: -60px;
inset-inline-start: 4px;
inset-inline-end: 4px;
padding: 12px;
height: 56px;
opacity: 0;
transition: opacity 1s ease-in-out;
transition: all 0.5s variables.$ease-out-local-preview;
&--visible {
opacity: 1;
bottom: 4px;
}
display: flex;
@@ -4780,13 +4794,20 @@ button.module-image__border-overlay:focus {
justify-content: space-around;
background-color: variables.$color-gray-78;
&__spacer {
flex-grow: 1;
flex-shrink: 1;
}
&__button {
flex-shrink: 0;
flex-grow: 0;
}
&__middle-button {
flex-grow: 1;
// icon width, and 15px of border on each side
width: 62px;
text-align: center;
flex-shrink: 0;
flex-grow: 0;
}
.CallingButton__icon {
@@ -4801,7 +4822,7 @@ button.module-image__border-overlay:focus {
inset-inline-end: 16px;
opacity: 0;
transition: opacity 1s ease-in-out;
transition: opacity 0.5s variables.$ease-out-local-preview;
&--visible {
opacity: 1;
+3
View File
@@ -8,12 +8,14 @@ export type PropsType = {
avatarUrl?: string;
children?: React.ReactNode;
className?: string;
darken?: boolean;
};
export function CallBackgroundBlur({
avatarUrl,
children,
className,
darken,
}: PropsType): JSX.Element {
return (
<div
@@ -31,6 +33,7 @@ export function CallBackgroundBlur({
}}
/>
)}
{darken && <div className="module-calling__background--darken" />}
{children}
</div>
);
+54 -16
View File
@@ -90,15 +90,19 @@ export type PropsType = {
toggleVideo: () => void;
};
const PIP_STARTING_HEIGHT = 286;
const PIP_WIDTH = 160;
const PIP_STARTING_HEIGHT_NORMAL = 286;
const PIP_STARTING_HEIGHT_LARGE = 400;
const LARGE_THRESHOLD = 1200;
export const PIP_WIDTH_NORMAL = 160;
const PIP_WIDTH_LARGE = 224;
const PIP_TOP_MARGIN = 78;
const PIP_PADDING = 8;
// Receiving portrait video will cause the PIP to update to match that video size, but
// we need limits
export const PIP_MINIMUM_HEIGHT = 180;
export const PIP_MAXIMUM_HEIGHT = 360;
export const PIP_MINIMUM_HEIGHT_MULTIPLIER = 1.2;
export const PIP_MAXIMUM_HEIGHT_MULTIPLIER = 2;
export function CallingPip({
activeCall,
@@ -120,7 +124,6 @@ export function CallingPip({
const videoContainerRef = React.useRef<null | HTMLDivElement>(null);
const [height, setHeight] = React.useState(PIP_STARTING_HEIGHT);
const [windowWidth, setWindowWidth] = React.useState(window.innerWidth);
const [windowHeight, setWindowHeight] = React.useState(window.innerHeight);
const [positionState, setPositionState] = React.useState<PositionState>({
@@ -128,6 +131,14 @@ export function CallingPip({
offsetY: PIP_TOP_MARGIN,
});
const isWindowLarge = windowWidth >= LARGE_THRESHOLD;
const [height, setHeight] = React.useState(
isWindowLarge ? PIP_STARTING_HEIGHT_LARGE : PIP_STARTING_HEIGHT_NORMAL
);
const [width, setWidth] = React.useState(
isWindowLarge ? PIP_WIDTH_LARGE : PIP_WIDTH_NORMAL
);
useActivateSpeakerViewOnPresenting({
remoteParticipants: activeCall.remoteParticipants,
switchToPresentationView,
@@ -164,11 +175,11 @@ export function CallingPip({
let distanceToLeftEdge: number;
let distanceToRightEdge: number;
if (isRTL) {
distanceToLeftEdge = innerWidth - (offsetX + PIP_WIDTH);
distanceToLeftEdge = innerWidth - (offsetX + width);
distanceToRightEdge = offsetX;
} else {
distanceToLeftEdge = offsetX;
distanceToRightEdge = innerWidth - (offsetX + PIP_WIDTH);
distanceToRightEdge = innerWidth - (offsetX + width);
}
const snapCandidates: Array<SnapCandidate> = [
@@ -208,14 +219,14 @@ export function CallingPip({
case PositionMode.SnapToBottom:
setPositionState({
mode: snapTo.mode,
offsetX: isRTL ? innerWidth - (offsetX + PIP_WIDTH) : offsetX,
offsetX: isRTL ? innerWidth - (offsetX + width) : offsetX,
});
break;
default:
throw missingCaseError(snapTo.mode);
}
}
}, [height, isRTL, positionState, setPositionState]);
}, [height, isRTL, positionState, setPositionState, width]);
React.useEffect(() => {
if (positionState.mode === PositionMode.BeingDragged) {
@@ -248,6 +259,17 @@ export function CallingPip({
};
}, []);
// This only runs when isWindowLarge changes, so we aggressively change height + width
React.useEffect(() => {
if (isWindowLarge) {
setHeight(PIP_STARTING_HEIGHT_LARGE);
setWidth(PIP_WIDTH_LARGE);
} else {
setHeight(PIP_STARTING_HEIGHT_NORMAL);
setWidth(PIP_WIDTH_NORMAL);
}
}, [isWindowLarge, setHeight, setWidth]);
const [translateX, translateY] = React.useMemo<[number, number]>(() => {
const topMin = PIP_TOP_MARGIN;
const bottomMax = windowHeight - PIP_PADDING - height;
@@ -256,7 +278,7 @@ export function CallingPip({
const leftMin = PIP_PADDING + leftScrollPadding;
const rightScrollPadding = isRTL ? 0 : 1;
const rightMax = windowWidth - PIP_PADDING - PIP_WIDTH - rightScrollPadding;
const rightMax = windowWidth - PIP_PADDING - width - rightScrollPadding;
switch (positionState.mode) {
case PositionMode.BeingDragged:
@@ -264,7 +286,7 @@ export function CallingPip({
isRTL
? windowWidth -
positionState.mouseX -
(PIP_WIDTH - positionState.dragOffsetX)
(width - positionState.dragOffsetX)
: positionState.mouseX - positionState.dragOffsetX,
positionState.mouseY - positionState.dragOffsetY,
];
@@ -291,7 +313,7 @@ export function CallingPip({
default:
throw missingCaseError(positionState);
}
}, [height, isRTL, windowWidth, windowHeight, positionState]);
}, [height, isRTL, width, windowWidth, windowHeight, positionState]);
const localizedTranslateX = isRTL ? -translateX : translateX;
const [showControls, setShowControls] = React.useState(false);
@@ -356,13 +378,17 @@ export function CallingPip({
const isLonelyInCall = !activeCall.remoteParticipants.length;
const isSendingVideo =
activeCall.hasLocalVideo || activeCall.presentingSource;
const avatarSize = isWindowLarge
? AvatarSize.NINETY_SIX
: AvatarSize.SIXTY_FOUR;
if (isLonelyInCall) {
remoteVideoNode = (
<div className="module-calling-pip__video--remote">
{isSendingVideo ? (
// TODO: DESKTOP-8537 - when black bars go away, need to make some CSS changes
<>
<CallBackgroundBlur avatarUrl={me.avatarUrl} />
<CallBackgroundBlur avatarUrl={me.avatarUrl} darken />
<div
className={classNames(
'module-calling-pip__full-size-local-preview',
@@ -387,7 +413,7 @@ export function CallingPip({
phoneNumber={me.phoneNumber}
profileName={me.profileName}
title={me.title}
size={AvatarSize.FORTY_EIGHT}
size={avatarSize}
sharedGroupNames={[]}
/>
</div>
@@ -405,13 +431,15 @@ export function CallingPip({
setRendererCanvas={setRendererCanvas}
setGroupCallVideoRequest={setGroupCallVideoRequest}
height={height}
width={PIP_WIDTH}
width={width}
updateHeight={(newHeight: number) => {
setHeight(newHeight);
}}
/>
);
}
const localVideoWidth = isWindowLarge ? 120 : 80;
const localVideoHeight = isWindowLarge ? 80 : 54;
return (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
@@ -449,6 +477,7 @@ export function CallingPip({
ref={videoContainerRef}
style={{
height: `${height}px`,
width: `${width}px`,
cursor:
positionState.mode === PositionMode.BeingDragged
? '-webkit-grabbing'
@@ -463,7 +492,14 @@ export function CallingPip({
{remoteVideoNode}
{!isLonelyInCall && activeCall.hasLocalVideo ? (
<div className={localVideoClassName} ref={setLocalPreviewContainer} />
<div
style={{
height: `${localVideoHeight}px`,
width: `${localVideoWidth}px`,
}}
className={localVideoClassName}
ref={setLocalPreviewContainer}
/>
) : null}
<div
@@ -520,6 +556,7 @@ export function CallingPip({
showControls ? 'module-calling-pip__actions--visible' : undefined
)}
>
<div className="module-calling-pip__actions__spacer" />
<div className="module-calling-pip__actions__button">
<CallingButton
buttonType={videoButtonType}
@@ -550,6 +587,7 @@ export function CallingPip({
tooltipDirection={TooltipPlacement.Top}
/>
</div>
<div className="module-calling-pip__actions__spacer" />
</div>
</div>
);
+37 -13
View File
@@ -27,15 +27,23 @@ import { isReconnecting } from '../util/callingIsReconnecting';
import { isGroupOrAdhocActiveCall } from '../util/isGroupOrAdhocCall';
import { assertDev } from '../util/assert';
import type { CallingImageDataCache } from './CallManager';
import { PIP_MAXIMUM_HEIGHT, PIP_MINIMUM_HEIGHT } from './CallingPip';
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 {
@@ -51,7 +59,7 @@ function BlurredBackground({
activeGroupCallSpeaker?.avatarUrl ?? activeCall.conversation.avatarUrl;
return (
<CallBackgroundBlur avatarUrl={avatarUrl}>
<CallBackgroundBlur avatarUrl={avatarUrl} darken={darken}>
<div className="module-calling-pip__video--avatar">
<Avatar
avatarPlaceholderGradient={avatarPlaceholderGradient}
@@ -64,7 +72,7 @@ function BlurredBackground({
phoneNumber={phoneNumber}
profileName={profileName}
title={title}
size={AvatarSize.FORTY_EIGHT}
size={avatarSize}
sharedGroupNames={sharedGroupNames}
/>
</div>
@@ -129,16 +137,14 @@ export function CallingPipRemoteVideo({
return;
}
const newHeight = clamp(
Math.floor(width * (1 / videoAspectRatio)),
1,
MAX_FRAME_HEIGHT
);
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 &&
newHeight >= PIP_MINIMUM_HEIGHT &&
newHeight <= PIP_MAXIMUM_HEIGHT
ratio >= PIP_MINIMUM_HEIGHT_MULTIPLIER &&
ratio <= PIP_MAXIMUM_HEIGHT_MULTIPLIER
) {
updateHeight(newHeight);
}
@@ -179,13 +185,20 @@ export function CallingPipRemoteVideo({
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} i18n={i18n} />
<BlurredBackground
activeCall={activeCall}
avatarSize={avatarSize}
i18n={i18n}
/>
</div>
);
}
@@ -196,7 +209,12 @@ export function CallingPipRemoteVideo({
// 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} i18n={i18n} />
<BlurredBackground
activeCall={activeCall}
avatarSize={avatarSize}
darken
i18n={i18n}
/>
<DirectCallRemoteParticipant
conversation={conversation}
hasRemoteVideo={hasRemoteVideo}
@@ -212,7 +230,11 @@ export function CallingPipRemoteVideo({
if (!activeGroupCallSpeaker) {
return (
<div className="module-calling-pip__video--remote">
<BlurredBackground activeCall={activeCall} i18n={i18n} />
<BlurredBackground
activeCall={activeCall}
avatarSize={avatarSize}
i18n={i18n}
/>
</div>
);
}
@@ -221,6 +243,8 @@ export function CallingPipRemoteVideo({
<BlurredBackground
activeCall={activeCall}
activeGroupCallSpeaker={activeGroupCallSpeaker}
avatarSize={avatarSize}
darken={activeGroupCallSpeaker.hasRemoteVideo}
i18n={i18n}
/>
<GroupCallRemoteParticipant