Highlight speaker on group calls

This commit is contained in:
Alvaro 2023-02-28 13:01:52 -07:00 committed by GitHub
parent 3d4248e070
commit 23cbd2c8b3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 80 additions and 25 deletions

View file

@ -3972,6 +3972,21 @@ button.module-image__border-overlay:focus {
transform: translate(0, 0);
transition: transform 200ms linear, width 200ms linear, height 200ms linear;
&:after {
content: '';
position: absolute;
width: 100%;
height: 100%;
border: 0 solid transparent;
border-radius: 5px;
transition: border-width 200ms, border-color 200ms;
transition-timing-function: ease-in-out;
}
&--speaking:after {
border-width: 3px;
border-color: $color-white;
}
&__remote-video {
// The background-color is seen while the video loads.
background-color: $color-gray-75;

View file

@ -40,11 +40,15 @@ import { NeedsScreenRecordingPermissionsModal } from './NeedsScreenRecordingPerm
import { missingCaseError } from '../util/missingCaseError';
import * as KeyboardLayout from '../services/keyboardLayout';
import { useActivateSpeakerViewOnPresenting } from '../hooks/useActivateSpeakerViewOnPresenting';
import { CallingAudioIndicator } from './CallingAudioIndicator';
import {
CallingAudioIndicator,
SPEAKING_LINGER_MS,
} from './CallingAudioIndicator';
import {
useActiveCallShortcuts,
useKeyboardShortcuts,
} from '../hooks/useKeyboardShortcuts';
import { useValueAtFixedRate } from '../hooks/useValueAtFixedRate';
export type PropsType = {
activeCall: ActiveCallType;
@ -155,6 +159,11 @@ export function CallScreen({
showParticipantsList,
} = activeCall;
const isSpeaking = useValueAtFixedRate(
localAudioLevel > 0,
SPEAKING_LINGER_MS
);
useActivateSpeakerViewOnPresenting({
remoteParticipants,
switchToPresentationView,
@ -536,6 +545,7 @@ export function CallScreen({
<CallingAudioIndicator
hasAudio={hasLocalAudio}
audioLevel={localAudioLevel}
shouldShowSpeaking={isSpeaking}
/>
</div>
</div>

View file

@ -4,8 +4,12 @@
import React, { useState, useEffect } from 'react';
import { boolean } from '@storybook/addon-knobs';
import { CallingAudioIndicator } from './CallingAudioIndicator';
import {
CallingAudioIndicator,
SPEAKING_LINGER_MS,
} from './CallingAudioIndicator';
import { AUDIO_LEVEL_INTERVAL_MS } from '../calling/constants';
import { useValueAtFixedRate } from '../hooks/useValueAtFixedRate';
export default {
title: 'Components/CallingAudioIndicator',
@ -24,10 +28,13 @@ export function Extreme(): JSX.Element {
};
}, [audioLevel, setAudioLevel]);
const isSpeaking = useValueAtFixedRate(audioLevel > 0, SPEAKING_LINGER_MS);
return (
<CallingAudioIndicator
hasAudio={boolean('hasAudio', true)}
audioLevel={audioLevel}
shouldShowSpeaking={isSpeaking}
/>
);
}
@ -45,10 +52,13 @@ export function Random(): JSX.Element {
};
}, [audioLevel, setAudioLevel]);
const isSpeaking = useValueAtFixedRate(audioLevel > 0, SPEAKING_LINGER_MS);
return (
<CallingAudioIndicator
hasAudio={boolean('hasAudio', true)}
audioLevel={audioLevel}
shouldShowSpeaking={isSpeaking}
/>
);
}

View file

@ -2,16 +2,14 @@
// SPDX-License-Identifier: AGPL-3.0-only
import classNames from 'classnames';
import { noop } from 'lodash';
import type { ReactElement } from 'react';
import React, { useEffect, useState } from 'react';
import React, { useEffect } from 'react';
import { useSpring, animated } from '@react-spring/web';
import { AUDIO_LEVEL_INTERVAL_MS } from '../calling/constants';
import { missingCaseError } from '../util/missingCaseError';
const SPEAKING_LINGER_MS = 500;
export const SPEAKING_LINGER_MS = 500;
const BASE_CLASS_NAME = 'CallingAudioIndicator';
const CONTENT_CLASS_NAME = `${BASE_CLASS_NAME}__content`;
@ -104,23 +102,12 @@ function Bars({ audioLevel }: { audioLevel: number }): ReactElement {
export function CallingAudioIndicator({
hasAudio,
audioLevel,
}: Readonly<{ hasAudio: boolean; audioLevel: number }>): ReactElement {
const [shouldShowSpeaking, setShouldShowSpeaking] = useState(audioLevel > 0);
useEffect(() => {
if (audioLevel > 0) {
setShouldShowSpeaking(true);
} else if (shouldShowSpeaking) {
const timeout = setTimeout(() => {
setShouldShowSpeaking(false);
}, SPEAKING_LINGER_MS);
return () => {
clearTimeout(timeout);
};
}
return noop;
}, [audioLevel, shouldShowSpeaking]);
shouldShowSpeaking,
}: Readonly<{
hasAudio: boolean;
audioLevel: number;
shouldShowSpeaking: boolean;
}>): ReactElement {
if (!hasAudio) {
return (
<div

View file

@ -16,13 +16,17 @@ import type { GroupCallRemoteParticipantType } from '../types/Calling';
import type { LocalizerType } from '../types/Util';
import { AvatarColors } from '../types/Colors';
import { CallBackgroundBlur } from './CallBackgroundBlur';
import { CallingAudioIndicator } from './CallingAudioIndicator';
import {
CallingAudioIndicator,
SPEAKING_LINGER_MS,
} from './CallingAudioIndicator';
import { Avatar, AvatarSize } from './Avatar';
import { ConfirmationDialog } from './ConfirmationDialog';
import { Intl } from './Intl';
import { ContactName } from './conversation/ContactName';
import { useIntersectionObserver } from '../hooks/useIntersectionObserver';
import { MAX_FRAME_HEIGHT, MAX_FRAME_WIDTH } from '../calling/constants';
import { useValueAtFixedRate } from '../hooks/useValueAtFixedRate';
const MAX_TIME_TO_SHOW_STALE_VIDEO_FRAMES = 5000;
const MAX_TIME_TO_SHOW_STALE_SCREENSHARE_FRAMES = 60000;
@ -79,6 +83,11 @@ export const GroupCallRemoteParticipant: React.FC<PropsType> = React.memo(
videoAspectRatio,
} = props.remoteParticipant;
const isSpeaking = useValueAtFixedRate(
!props.isInPip ? props.audioLevel > 0 : false,
SPEAKING_LINGER_MS
);
const [hasReceivedVideoRecently, setHasReceivedVideoRecently] =
useState(false);
const [isWide, setIsWide] = useState<boolean>(
@ -266,7 +275,11 @@ export const GroupCallRemoteParticipant: React.FC<PropsType> = React.memo(
)}
<div
className="module-ongoing-call__group-call-remote-participant"
className={classNames(
'module-ongoing-call__group-call-remote-participant',
isSpeaking &&
'module-ongoing-call__group-call-remote-participant--speaking'
)}
ref={intersectionRef}
style={containerStyles}
>
@ -283,6 +296,7 @@ export const GroupCallRemoteParticipant: React.FC<PropsType> = React.memo(
<CallingAudioIndicator
hasAudio={hasRemoteAudio}
audioLevel={props.audioLevel}
shouldShowSpeaking={isSpeaking}
/>
</div>
)}

View file

@ -0,0 +1,19 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { useEffect, useState } from 'react';
export function useValueAtFixedRate<T>(value: T, rate: number): T {
const [currentValue, setCurrentValue] = useState(value);
useEffect(() => {
const timeout = setTimeout(() => {
setCurrentValue(value);
}, rate);
return () => {
clearTimeout(timeout);
};
}, [value, rate]);
return currentValue;
}