Blur participant videos when calls are reconnecting
This commit is contained in:
parent
4ea0970e54
commit
777b9d52e9
11 changed files with 68 additions and 21 deletions
|
@ -3655,6 +3655,9 @@ button.module-image__border-overlay:focus {
|
||||||
background-color: $color-gray-95;
|
background-color: $color-gray-95;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
&--reconnecting {
|
||||||
|
filter: blur(15px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__remote-video-disabled {
|
&__remote-video-disabled {
|
||||||
|
@ -3844,6 +3847,9 @@ button.module-image__border-overlay:focus {
|
||||||
&__remote-video {
|
&__remote-video {
|
||||||
// The background-color is seen while the video loads.
|
// The background-color is seen while the video loads.
|
||||||
background-color: $color-gray-75;
|
background-color: $color-gray-75;
|
||||||
|
&--reconnecting {
|
||||||
|
filter: blur(15px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__blocked {
|
&__blocked {
|
||||||
|
|
|
@ -49,6 +49,7 @@ import {
|
||||||
useKeyboardShortcuts,
|
useKeyboardShortcuts,
|
||||||
} from '../hooks/useKeyboardShortcuts';
|
} from '../hooks/useKeyboardShortcuts';
|
||||||
import { useValueAtFixedRate } from '../hooks/useValueAtFixedRate';
|
import { useValueAtFixedRate } from '../hooks/useValueAtFixedRate';
|
||||||
|
import { isReconnecting } from '../util/callingIsReconnecting';
|
||||||
|
|
||||||
export type PropsType = {
|
export type PropsType = {
|
||||||
activeCall: ActiveCallType;
|
activeCall: ActiveCallType;
|
||||||
|
@ -113,9 +114,6 @@ function DirectCallHeaderMessage({
|
||||||
return clearInterval.bind(null, interval);
|
return clearInterval.bind(null, interval);
|
||||||
}, [joinedAt]);
|
}, [joinedAt]);
|
||||||
|
|
||||||
if (callState === CallState.Reconnecting) {
|
|
||||||
return <>{i18n('icu:callReconnecting')}</>;
|
|
||||||
}
|
|
||||||
if (callState === CallState.Accepted && acceptedDuration) {
|
if (callState === CallState.Accepted && acceptedDuration) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -298,6 +296,7 @@ export function CallScreen({
|
||||||
conversation={conversation}
|
conversation={conversation}
|
||||||
hasRemoteVideo={hasRemoteVideo}
|
hasRemoteVideo={hasRemoteVideo}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
|
isReconnecting={isReconnecting(activeCall)}
|
||||||
setRendererCanvas={setRendererCanvas}
|
setRendererCanvas={setRendererCanvas}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
@ -333,6 +332,7 @@ export function CallScreen({
|
||||||
remoteParticipants={activeCall.remoteParticipants}
|
remoteParticipants={activeCall.remoteParticipants}
|
||||||
setGroupCallVideoRequest={setGroupCallVideoRequest}
|
setGroupCallVideoRequest={setGroupCallVideoRequest}
|
||||||
remoteAudioLevels={activeCall.remoteAudioLevels}
|
remoteAudioLevels={activeCall.remoteAudioLevels}
|
||||||
|
isCallReconnecting={isReconnecting(activeCall)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
@ -343,15 +343,9 @@ export function CallScreen({
|
||||||
let lonelyInCallNode: ReactNode;
|
let lonelyInCallNode: ReactNode;
|
||||||
let localPreviewNode: ReactNode;
|
let localPreviewNode: ReactNode;
|
||||||
|
|
||||||
const isLonelyInGroup =
|
const isLonelyInCall = !activeCall.remoteParticipants.length;
|
||||||
activeCall.callMode === CallMode.Group &&
|
|
||||||
!activeCall.remoteParticipants.length;
|
|
||||||
|
|
||||||
const isLonelyInDirectCall =
|
if (isLonelyInCall) {
|
||||||
activeCall.callMode === CallMode.Direct &&
|
|
||||||
activeCall.callState !== CallState.Accepted;
|
|
||||||
|
|
||||||
if (isLonelyInGroup || isLonelyInDirectCall) {
|
|
||||||
lonelyInCallNode = (
|
lonelyInCallNode = (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
|
|
|
@ -22,6 +22,7 @@ import { MAX_FRAME_WIDTH } from '../calling/constants';
|
||||||
import { usePageVisibility } from '../hooks/usePageVisibility';
|
import { usePageVisibility } from '../hooks/usePageVisibility';
|
||||||
import { missingCaseError } from '../util/missingCaseError';
|
import { missingCaseError } from '../util/missingCaseError';
|
||||||
import { nonRenderedRemoteParticipant } from '../util/ringrtc/nonRenderedRemoteParticipant';
|
import { nonRenderedRemoteParticipant } from '../util/ringrtc/nonRenderedRemoteParticipant';
|
||||||
|
import { isReconnecting } from '../util/callingIsReconnecting';
|
||||||
|
|
||||||
// This value should be kept in sync with the hard-coded CSS height. It should also be
|
// This value should be kept in sync with the hard-coded CSS height. It should also be
|
||||||
// less than `MAX_FRAME_HEIGHT`.
|
// less than `MAX_FRAME_HEIGHT`.
|
||||||
|
@ -154,6 +155,7 @@ export function CallingPipRemoteVideo({
|
||||||
conversation={conversation}
|
conversation={conversation}
|
||||||
hasRemoteVideo={hasRemoteVideo}
|
hasRemoteVideo={hasRemoteVideo}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
|
isReconnecting={isReconnecting(activeCall)}
|
||||||
setRendererCanvas={setRendererCanvas}
|
setRendererCanvas={setRendererCanvas}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -173,6 +175,7 @@ export function CallingPipRemoteVideo({
|
||||||
remoteParticipant={activeGroupCallSpeaker}
|
remoteParticipant={activeGroupCallSpeaker}
|
||||||
remoteParticipantsCount={activeCall.remoteParticipants.length}
|
remoteParticipantsCount={activeCall.remoteParticipants.length}
|
||||||
isActiveSpeakerInSpeakerView={false}
|
isActiveSpeakerInSpeakerView={false}
|
||||||
|
isCallReconnecting={isReconnecting(activeCall)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -3,11 +3,12 @@
|
||||||
|
|
||||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import type { ActiveCallType } from '../types/Calling';
|
import type { ActiveCallType } from '../types/Calling';
|
||||||
import { CallMode, GroupCallConnectionState } from '../types/Calling';
|
import { CallMode } from '../types/Calling';
|
||||||
import type { ConversationType } from '../state/ducks/conversations';
|
import type { ConversationType } from '../state/ducks/conversations';
|
||||||
import type { LocalizerType } from '../types/Util';
|
import type { LocalizerType } from '../types/Util';
|
||||||
import { clearTimeoutIfNecessary } from '../util/clearTimeoutIfNecessary';
|
import { clearTimeoutIfNecessary } from '../util/clearTimeoutIfNecessary';
|
||||||
import { CallingToast, DEFAULT_LIFETIME } from './CallingToast';
|
import { CallingToast, DEFAULT_LIFETIME } from './CallingToast';
|
||||||
|
import { isReconnecting } from '../util/callingIsReconnecting';
|
||||||
|
|
||||||
type PropsType = {
|
type PropsType = {
|
||||||
activeCall: ActiveCallType;
|
activeCall: ActiveCallType;
|
||||||
|
@ -22,10 +23,7 @@ type ToastType =
|
||||||
| undefined;
|
| undefined;
|
||||||
|
|
||||||
function getReconnectingToast({ activeCall, i18n }: PropsType): ToastType {
|
function getReconnectingToast({ activeCall, i18n }: PropsType): ToastType {
|
||||||
if (
|
if (isReconnecting(activeCall)) {
|
||||||
activeCall.callMode === CallMode.Group &&
|
|
||||||
activeCall.connectionState === GroupCallConnectionState.Reconnecting
|
|
||||||
) {
|
|
||||||
return {
|
return {
|
||||||
message: i18n('icu:callReconnecting'),
|
message: i18n('icu:callReconnecting'),
|
||||||
type: 'static',
|
type: 'static',
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import React, { useRef, useEffect } from 'react';
|
import React, { useRef, useEffect } from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
import type { SetRendererCanvasType } from '../state/ducks/calling';
|
import type { SetRendererCanvasType } from '../state/ducks/calling';
|
||||||
import type { ConversationType } from '../state/ducks/conversations';
|
import type { ConversationType } from '../state/ducks/conversations';
|
||||||
import type { LocalizerType } from '../types/Util';
|
import type { LocalizerType } from '../types/Util';
|
||||||
|
@ -12,6 +13,7 @@ type PropsType = {
|
||||||
conversation: ConversationType;
|
conversation: ConversationType;
|
||||||
hasRemoteVideo: boolean;
|
hasRemoteVideo: boolean;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
|
isReconnecting: boolean;
|
||||||
setRendererCanvas: (_: SetRendererCanvasType) => void;
|
setRendererCanvas: (_: SetRendererCanvasType) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -19,6 +21,7 @@ export function DirectCallRemoteParticipant({
|
||||||
conversation,
|
conversation,
|
||||||
hasRemoteVideo,
|
hasRemoteVideo,
|
||||||
i18n,
|
i18n,
|
||||||
|
isReconnecting,
|
||||||
setRendererCanvas,
|
setRendererCanvas,
|
||||||
}: PropsType): JSX.Element {
|
}: PropsType): JSX.Element {
|
||||||
const remoteVideoRef = useRef<HTMLCanvasElement | null>(null);
|
const remoteVideoRef = useRef<HTMLCanvasElement | null>(null);
|
||||||
|
@ -32,7 +35,11 @@ export function DirectCallRemoteParticipant({
|
||||||
|
|
||||||
return hasRemoteVideo ? (
|
return hasRemoteVideo ? (
|
||||||
<canvas
|
<canvas
|
||||||
className="module-ongoing-call__remote-video-enabled"
|
className={classNames(
|
||||||
|
'module-ongoing-call__remote-video-enabled',
|
||||||
|
isReconnecting &&
|
||||||
|
'module-ongoing-call__remote-video-enabled--reconnecting'
|
||||||
|
)}
|
||||||
ref={remoteVideoRef}
|
ref={remoteVideoRef}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -42,6 +42,7 @@ const defaultProps = {
|
||||||
getFrameBuffer: memoize(() => Buffer.alloc(FRAME_BUFFER_SIZE)),
|
getFrameBuffer: memoize(() => Buffer.alloc(FRAME_BUFFER_SIZE)),
|
||||||
getGroupCallVideoFrameSource: fakeGetGroupCallVideoFrameSource,
|
getGroupCallVideoFrameSource: fakeGetGroupCallVideoFrameSource,
|
||||||
i18n,
|
i18n,
|
||||||
|
isCallReconnecting: false,
|
||||||
onParticipantVisibilityChanged: action('onParticipantVisibilityChanged'),
|
onParticipantVisibilityChanged: action('onParticipantVisibilityChanged'),
|
||||||
remoteAudioLevels: new Map<number, number>(),
|
remoteAudioLevels: new Map<number, number>(),
|
||||||
remoteParticipantsCount: 1,
|
remoteParticipantsCount: 1,
|
||||||
|
|
|
@ -19,6 +19,7 @@ export type PropsType = {
|
||||||
getFrameBuffer: () => Buffer;
|
getFrameBuffer: () => Buffer;
|
||||||
getGroupCallVideoFrameSource: (demuxId: number) => VideoFrameSource;
|
getGroupCallVideoFrameSource: (demuxId: number) => VideoFrameSource;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
|
isCallReconnecting: boolean;
|
||||||
onParticipantVisibilityChanged: (
|
onParticipantVisibilityChanged: (
|
||||||
demuxId: number,
|
demuxId: number,
|
||||||
isVisible: boolean
|
isVisible: boolean
|
||||||
|
@ -32,6 +33,7 @@ export function GroupCallOverflowArea({
|
||||||
getFrameBuffer,
|
getFrameBuffer,
|
||||||
getGroupCallVideoFrameSource,
|
getGroupCallVideoFrameSource,
|
||||||
i18n,
|
i18n,
|
||||||
|
isCallReconnecting,
|
||||||
onParticipantVisibilityChanged,
|
onParticipantVisibilityChanged,
|
||||||
overflowedParticipants,
|
overflowedParticipants,
|
||||||
remoteAudioLevels,
|
remoteAudioLevels,
|
||||||
|
@ -127,6 +129,7 @@ export function GroupCallOverflowArea({
|
||||||
remoteParticipant={remoteParticipant}
|
remoteParticipant={remoteParticipant}
|
||||||
remoteParticipantsCount={remoteParticipantsCount}
|
remoteParticipantsCount={remoteParticipantsCount}
|
||||||
isActiveSpeakerInSpeakerView={false}
|
isActiveSpeakerInSpeakerView={false}
|
||||||
|
isCallReconnecting={isCallReconnecting}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -67,6 +67,7 @@ const createProps = (
|
||||||
},
|
},
|
||||||
remoteParticipantsCount: 1,
|
remoteParticipantsCount: 1,
|
||||||
isActiveSpeakerInSpeakerView: false,
|
isActiveSpeakerInSpeakerView: false,
|
||||||
|
isCallReconnecting: false,
|
||||||
...overrideProps,
|
...overrideProps,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@ import { useIntersectionObserver } from '../hooks/useIntersectionObserver';
|
||||||
import { MAX_FRAME_HEIGHT, MAX_FRAME_WIDTH } from '../calling/constants';
|
import { MAX_FRAME_HEIGHT, MAX_FRAME_WIDTH } from '../calling/constants';
|
||||||
import { useValueAtFixedRate } from '../hooks/useValueAtFixedRate';
|
import { useValueAtFixedRate } from '../hooks/useValueAtFixedRate';
|
||||||
|
|
||||||
const MAX_TIME_TO_SHOW_STALE_VIDEO_FRAMES = 5000;
|
const MAX_TIME_TO_SHOW_STALE_VIDEO_FRAMES = 10000;
|
||||||
const MAX_TIME_TO_SHOW_STALE_SCREENSHARE_FRAMES = 60000;
|
const MAX_TIME_TO_SHOW_STALE_SCREENSHARE_FRAMES = 60000;
|
||||||
|
|
||||||
type BasePropsType = {
|
type BasePropsType = {
|
||||||
|
@ -36,6 +36,7 @@ type BasePropsType = {
|
||||||
getGroupCallVideoFrameSource: (demuxId: number) => VideoFrameSource;
|
getGroupCallVideoFrameSource: (demuxId: number) => VideoFrameSource;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
isActiveSpeakerInSpeakerView: boolean;
|
isActiveSpeakerInSpeakerView: boolean;
|
||||||
|
isCallReconnecting: boolean;
|
||||||
onVisibilityChanged?: (demuxId: number, isVisible: boolean) => unknown;
|
onVisibilityChanged?: (demuxId: number, isVisible: boolean) => unknown;
|
||||||
remoteParticipant: GroupCallRemoteParticipantType;
|
remoteParticipant: GroupCallRemoteParticipantType;
|
||||||
remoteParticipantsCount: number;
|
remoteParticipantsCount: number;
|
||||||
|
@ -69,6 +70,7 @@ export const GroupCallRemoteParticipant: React.FC<PropsType> = React.memo(
|
||||||
onVisibilityChanged,
|
onVisibilityChanged,
|
||||||
remoteParticipantsCount,
|
remoteParticipantsCount,
|
||||||
isActiveSpeakerInSpeakerView,
|
isActiveSpeakerInSpeakerView,
|
||||||
|
isCallReconnecting,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
@ -136,7 +138,13 @@ export const GroupCallRemoteParticipant: React.FC<PropsType> = React.memo(
|
||||||
? MAX_TIME_TO_SHOW_STALE_SCREENSHARE_FRAMES
|
? MAX_TIME_TO_SHOW_STALE_SCREENSHARE_FRAMES
|
||||||
: MAX_TIME_TO_SHOW_STALE_VIDEO_FRAMES;
|
: MAX_TIME_TO_SHOW_STALE_VIDEO_FRAMES;
|
||||||
if (frameAge > maxFrameAge) {
|
if (frameAge > maxFrameAge) {
|
||||||
setHasReceivedVideoRecently(false);
|
// We consider that we have received video recently from a remote participant if
|
||||||
|
// we have received it recently relative to the last time we had a connection. If
|
||||||
|
// we lost their video due to our reconnecting, we still want to show the last
|
||||||
|
// frame of video (blurred out) until we have reconnected.
|
||||||
|
if (!isCallReconnecting) {
|
||||||
|
setHasReceivedVideoRecently(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const canvasEl = remoteVideoRef.current;
|
const canvasEl = remoteVideoRef.current;
|
||||||
|
@ -191,7 +199,7 @@ export const GroupCallRemoteParticipant: React.FC<PropsType> = React.memo(
|
||||||
|
|
||||||
setHasReceivedVideoRecently(true);
|
setHasReceivedVideoRecently(true);
|
||||||
setIsWide(frameWidth > frameHeight);
|
setIsWide(frameWidth > frameHeight);
|
||||||
}, [getFrameBuffer, videoFrameSource, sharingScreen]);
|
}, [getFrameBuffer, videoFrameSource, sharingScreen, isCallReconnecting]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!hasRemoteVideo) {
|
if (!hasRemoteVideo) {
|
||||||
|
@ -310,7 +318,11 @@ export const GroupCallRemoteParticipant: React.FC<PropsType> = React.memo(
|
||||||
)}
|
)}
|
||||||
{wantsToShowVideo && (
|
{wantsToShowVideo && (
|
||||||
<canvas
|
<canvas
|
||||||
className="module-ongoing-call__group-call-remote-participant__remote-video"
|
className={classNames(
|
||||||
|
'module-ongoing-call__group-call-remote-participant__remote-video',
|
||||||
|
isCallReconnecting &&
|
||||||
|
'module-ongoing-call__group-call-remote-participant__remote-video--reconnecting'
|
||||||
|
)}
|
||||||
style={{
|
style={{
|
||||||
...canvasStyles,
|
...canvasStyles,
|
||||||
// If we want to show video but don't have any yet, we still render the
|
// If we want to show video but don't have any yet, we still render the
|
||||||
|
|
|
@ -46,6 +46,7 @@ type GridArrangement = {
|
||||||
type PropsType = {
|
type PropsType = {
|
||||||
getGroupCallVideoFrameSource: (demuxId: number) => VideoFrameSource;
|
getGroupCallVideoFrameSource: (demuxId: number) => VideoFrameSource;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
|
isCallReconnecting: boolean;
|
||||||
isInSpeakerView: boolean;
|
isInSpeakerView: boolean;
|
||||||
remoteParticipants: ReadonlyArray<GroupCallRemoteParticipantType>;
|
remoteParticipants: ReadonlyArray<GroupCallRemoteParticipantType>;
|
||||||
setGroupCallVideoRequest: (
|
setGroupCallVideoRequest: (
|
||||||
|
@ -87,6 +88,7 @@ enum VideoRequestMode {
|
||||||
export function GroupCallRemoteParticipants({
|
export function GroupCallRemoteParticipants({
|
||||||
getGroupCallVideoFrameSource,
|
getGroupCallVideoFrameSource,
|
||||||
i18n,
|
i18n,
|
||||||
|
isCallReconnecting,
|
||||||
isInSpeakerView,
|
isInSpeakerView,
|
||||||
remoteParticipants,
|
remoteParticipants,
|
||||||
setGroupCallVideoRequest,
|
setGroupCallVideoRequest,
|
||||||
|
@ -297,6 +299,7 @@ export function GroupCallRemoteParticipants({
|
||||||
width={renderedWidth}
|
width={renderedWidth}
|
||||||
remoteParticipantsCount={remoteParticipants.length}
|
remoteParticipantsCount={remoteParticipants.length}
|
||||||
isActiveSpeakerInSpeakerView={isInSpeakerView}
|
isActiveSpeakerInSpeakerView={isInSpeakerView}
|
||||||
|
isCallReconnecting={isCallReconnecting}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -424,6 +427,7 @@ export function GroupCallRemoteParticipants({
|
||||||
getFrameBuffer={getFrameBuffer}
|
getFrameBuffer={getFrameBuffer}
|
||||||
getGroupCallVideoFrameSource={getGroupCallVideoFrameSource}
|
getGroupCallVideoFrameSource={getGroupCallVideoFrameSource}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
|
isCallReconnecting={isCallReconnecting}
|
||||||
onParticipantVisibilityChanged={onParticipantVisibilityChanged}
|
onParticipantVisibilityChanged={onParticipantVisibilityChanged}
|
||||||
overflowedParticipants={overflowedParticipants}
|
overflowedParticipants={overflowedParticipants}
|
||||||
remoteAudioLevels={remoteAudioLevels}
|
remoteAudioLevels={remoteAudioLevels}
|
||||||
|
|
18
ts/util/callingIsReconnecting.ts
Normal file
18
ts/util/callingIsReconnecting.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
// Copyright 2023 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import {
|
||||||
|
CallMode,
|
||||||
|
CallState,
|
||||||
|
GroupCallConnectionState,
|
||||||
|
} from '../types/Calling';
|
||||||
|
import type { ActiveCallType } from '../types/Calling';
|
||||||
|
|
||||||
|
export function isReconnecting(activeCall: ActiveCallType): boolean {
|
||||||
|
return (
|
||||||
|
(activeCall.callMode === CallMode.Group &&
|
||||||
|
activeCall.connectionState === GroupCallConnectionState.Reconnecting) ||
|
||||||
|
(activeCall.callMode === CallMode.Direct &&
|
||||||
|
activeCall.callState === CallState.Reconnecting)
|
||||||
|
);
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue