Blur participant videos when calls are reconnecting

This commit is contained in:
trevor-signal 2023-10-16 13:58:51 -04:00 committed by GitHub
parent 4ea0970e54
commit 777b9d52e9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 68 additions and 21 deletions

View file

@ -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 {

View file

@ -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(

View file

@ -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>
); );

View file

@ -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',

View file

@ -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}
/> />
) : ( ) : (

View file

@ -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,

View file

@ -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>

View file

@ -67,6 +67,7 @@ const createProps = (
}, },
remoteParticipantsCount: 1, remoteParticipantsCount: 1,
isActiveSpeakerInSpeakerView: false, isActiveSpeakerInSpeakerView: false,
isCallReconnecting: false,
...overrideProps, ...overrideProps,
}); });

View file

@ -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

View file

@ -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}

View 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)
);
}