From e6223b6a1156ca76a4eb66bbe0f198ceaa500cc4 Mon Sep 17 00:00:00 2001 From: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com> Date: Wed, 18 May 2022 20:28:51 -0700 Subject: [PATCH] Dynamic audio level indicator --- .../CallingSpeakingIndicator.json | 1 - .../components/CallingAudioIndicator.scss | 5 - ts/calling/constants.ts | 4 +- ts/calling/getAudioLevelForSpeaking.ts | 20 --- ts/calling/truncateAudioLevel.ts | 24 ++++ ts/components/CallManager.stories.tsx | 6 +- ts/components/CallScreen.stories.tsx | 10 +- ts/components/CallScreen.tsx | 6 +- .../CallingAudioIndicator.stories.tsx | 17 +++ ts/components/CallingAudioIndicator.tsx | 117 ++++++++++++++++-- ts/components/CallingPip.stories.tsx | 6 +- .../GroupCallOverflowArea.stories.tsx | 2 +- ts/components/GroupCallOverflowArea.tsx | 6 +- .../GroupCallRemoteParticipant.stories.tsx | 39 ++++-- ts/components/GroupCallRemoteParticipant.tsx | 4 +- ts/components/GroupCallRemoteParticipants.tsx | 10 +- ts/services/calling.ts | 5 - ts/state/ducks/calling.ts | 53 ++++---- ts/state/smart/CallManager.tsx | 4 +- ts/test-both/util/mapUtil_test.ts | 43 ++++++- ts/test-electron/state/ducks/calling_test.ts | 37 +++--- .../state/selectors/calling_test.ts | 2 +- ts/types/Calling.ts | 4 +- ts/util/mapUtil.ts | 21 ++++ 24 files changed, 323 insertions(+), 123 deletions(-) delete mode 100644 images/lottie-animations/CallingSpeakingIndicator.json delete mode 100644 ts/calling/getAudioLevelForSpeaking.ts create mode 100644 ts/calling/truncateAudioLevel.ts create mode 100644 ts/components/CallingAudioIndicator.stories.tsx diff --git a/images/lottie-animations/CallingSpeakingIndicator.json b/images/lottie-animations/CallingSpeakingIndicator.json deleted file mode 100644 index 8911ba99c..000000000 --- a/images/lottie-animations/CallingSpeakingIndicator.json +++ /dev/null @@ -1 +0,0 @@ -{"v":"5.5.10","fr":60,"ip":0,"op":60,"w":20,"h":20,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Bars","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[10,10,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[-0.828,0],[0,0.828],[0,0],[0.828,0],[0,-0.828]],"o":[[0,0.828],[0.828,0],[0,0],[0,-0.828],[-0.828,0],[0,0]],"v":[[-1.5,0],[0,1.5],[1.5,0],[1.5,0],[0,-1.5],[-1.5,0]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":15,"s":[{"i":[[0,0],[-0.828,0],[0,0.828],[0,0],[0.828,0],[0,-0.828]],"o":[[0,0.828],[0.828,0],[0,0],[0,-0.828],[-0.828,0],[0,0]],"v":[[-1.5,6.5],[0,8],[1.5,6.5],[1.5,-6.5],[0,-8],[-1.5,-6.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0,0],[-0.828,0],[0,0.828],[0,0],[0.828,0],[0,-0.828]],"o":[[0,0.828],[0.828,0],[0,0],[0,-0.828],[-0.828,0],[0,0]],"v":[[-1.5,0],[0,1.5],[1.5,0],[1.5,0],[0,-1.5],[-1.5,0]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":45,"s":[{"i":[[0,0],[-0.828,0],[0,0.828],[0,0],[0.828,0],[0,-0.828]],"o":[[0,0.828],[0.828,0],[0,0],[0,-0.828],[-0.828,0],[0,0]],"v":[[-1.5,2.5],[0,4],[1.5,2.5],[1.5,-2.5],[0,-4],[-1.5,-2.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":60,"s":[{"i":[[0,0],[-0.828,0],[0,0.828],[0,0],[0.828,0],[0,-0.828]],"o":[[0,0.828],[0.828,0],[0,0],[0,-0.828],[-0.828,0],[0,0]],"v":[[-1.5,0],[0,1.5],[1.5,0],[1.5,0],[0,-1.5],[-1.5,0]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":65,"s":[{"i":[[0,0],[-0.828,0],[0,0.828],[0,0],[0.828,0],[0,-0.828]],"o":[[0,0.828],[0.828,0],[0,0],[0,-0.828],[-0.828,0],[0,0]],"v":[[-1.5,2.5],[0,4],[1.5,2.5],[1.5,-2.5],[0,-4],[-1.5,-2.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":72,"s":[{"i":[[0,0],[-0.828,0],[0,0.828],[0,0],[0.828,0],[0,-0.828]],"o":[[0,0.828],[0.828,0],[0,0],[0,-0.828],[-0.828,0],[0,0]],"v":[[-1.5,4.5],[0,6],[1.5,4.5],[1.5,-4.5],[0,-6],[-1.5,-4.5]],"c":true}]},{"t":77,"s":[{"i":[[0,0],[-0.828,0],[0,0.828],[0,0],[0.828,0],[0,-0.828]],"o":[[0,0.828],[0.828,0],[0,0],[0,-0.828],[-0.828,0],[0,0]],"v":[[-1.5,2.5],[0,4],[1.5,2.5],[1.5,-2.5],[0,-4],[-1.5,-2.5]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[-0.828,0],[0,0.828],[0,0],[0.828,0],[0,-0.828]],"o":[[0,0.828],[0.828,0],[0,0],[0,-0.828],[-0.828,0],[0,0]],"v":[[-8,0],[-6.5,1.5],[-5,0],[-5,0],[-6.5,-1.5],[-8,0]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":15,"s":[{"i":[[0,0],[-0.828,0],[0,0.828],[0,0],[0.828,0],[0,-0.828]],"o":[[0,0.828],[0.828,0],[0,0],[0,-0.828],[-0.828,0],[0,0]],"v":[[-8.012,4.5],[-6.512,6],[-5.012,4.5],[-5.012,-4.5],[-6.512,-6],[-8.012,-4.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0,0],[-0.828,0],[0,0.828],[0,0],[0.828,0],[0,-0.828]],"o":[[0,0.828],[0.828,0],[0,0],[0,-0.828],[-0.828,0],[0,0]],"v":[[-8,0],[-6.5,1.5],[-5,0],[-5,0],[-6.5,-1.5],[-8,0]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":45,"s":[{"i":[[0,0],[-0.828,0],[0,0.828],[0,0],[0.828,0],[0,-0.828]],"o":[[0,0.828],[0.828,0],[0,0],[0,-0.828],[-0.828,0],[0,0]],"v":[[-8.012,4.5],[-6.512,6],[-5.012,4.5],[-5.012,-4.5],[-6.512,-6],[-8.012,-4.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":60,"s":[{"i":[[0,0],[-0.828,0],[0,0.828],[0,0],[0.828,0],[0,-0.828]],"o":[[0,0.828],[0.828,0],[0,0],[0,-0.828],[-0.828,0],[0,0]],"v":[[-8,0],[-6.5,1.5],[-5,0],[-5,0],[-6.5,-1.5],[-8,0]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":65,"s":[{"i":[[0,0],[-0.828,0],[0,0.828],[0,0],[0.828,0],[0,-0.828]],"o":[[0,0.828],[0.828,0],[0,0],[0,-0.828],[-0.828,0],[0,0]],"v":[[-8,0],[-6.5,1.5],[-5,0],[-5,0],[-6.5,-1.5],[-8,0]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":72,"s":[{"i":[[0,0],[-0.828,0],[0,0.828],[0,0],[0.828,0],[0,-0.828]],"o":[[0,0.828],[0.828,0],[0,0],[0,-0.828],[-0.828,0],[0,0]],"v":[[-8.012,2.5],[-6.512,4],[-5.012,2.5],[-5.012,-2.5],[-6.512,-4],[-8.012,-2.5]],"c":true}]},{"t":77,"s":[{"i":[[0,0],[-0.828,0],[0,0.828],[0,0],[0.828,0],[0,-0.828]],"o":[[0,0.828],[0.828,0],[0,0],[0,-0.828],[-0.828,0],[0,0]],"v":[[-8.012,4.5],[-6.512,6],[-5.012,4.5],[-5.012,-4.5],[-6.512,-6],[-8.012,-4.5]],"c":true}]}],"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[-0.828,0],[0,-0.828],[0,0],[0.828,0],[0,0.828]],"o":[[0,-0.828],[0.828,0],[0,0],[0,0.828],[-0.828,0],[0,0]],"v":[[5,0],[6.5,-1.5],[8,0],[8,0],[6.5,1.5],[5,0]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":15,"s":[{"i":[[0,0],[-0.828,0],[0,-0.828],[0,0],[0.828,0],[0,0.828]],"o":[[0,-0.828],[0.828,0],[0,0],[0,0.828],[-0.828,0],[0,0]],"v":[[4.988,-4.5],[6.488,-6],[7.988,-4.5],[7.988,4.5],[6.488,6],[4.988,4.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0,0],[-0.828,0],[0,-0.828],[0,0],[0.828,0],[0,0.828]],"o":[[0,-0.828],[0.828,0],[0,0],[0,0.828],[-0.828,0],[0,0]],"v":[[5,0],[6.5,-1.5],[8,0],[8,0],[6.5,1.5],[5,0]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":45,"s":[{"i":[[0,0],[-0.828,0],[0,-0.828],[0,0],[0.828,0],[0,0.828]],"o":[[0,-0.828],[0.828,0],[0,0],[0,0.828],[-0.828,0],[0,0]],"v":[[4.988,-4.5],[6.488,-6],[7.988,-4.5],[7.988,4.5],[6.488,6],[4.988,4.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":60,"s":[{"i":[[0,0],[-0.828,0],[0,-0.828],[0,0],[0.828,0],[0,0.828]],"o":[[0,-0.828],[0.828,0],[0,0],[0,0.828],[-0.828,0],[0,0]],"v":[[5,0],[6.5,-1.5],[8,0],[8,0],[6.5,1.5],[5,0]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":65,"s":[{"i":[[0,0],[-0.828,0],[0,-0.828],[0,0],[0.828,0],[0,0.828]],"o":[[0,-0.828],[0.828,0],[0,0],[0,0.828],[-0.828,0],[0,0]],"v":[[5,0],[6.5,-1.5],[8,0],[8,0],[6.5,1.5],[5,0]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":72,"s":[{"i":[[0,0],[-0.828,0],[0,-0.828],[0,0],[0.828,0],[0,0.828]],"o":[[0,-0.828],[0.828,0],[0,0],[0,0.828],[-0.828,0],[0,0]],"v":[[4.988,-2.5],[6.488,-4],[7.988,-2.5],[7.988,2.5],[6.488,4],[4.988,2.5]],"c":true}]},{"t":77,"s":[{"i":[[0,0],[-0.828,0],[0,-0.828],[0,0],[0.828,0],[0,0.828]],"o":[[0,-0.828],[0.828,0],[0,0],[0,0.828],[-0.828,0],[0,0]],"v":[[4.988,-4.5],[6.488,-6],[7.988,-4.5],[7.988,4.5],[6.488,6],[4.988,4.5]],"c":true}]}],"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":90,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/stylesheets/components/CallingAudioIndicator.scss b/stylesheets/components/CallingAudioIndicator.scss index 9a3a0be0d..c6adb8044 100644 --- a/stylesheets/components/CallingAudioIndicator.scss +++ b/stylesheets/components/CallingAudioIndicator.scss @@ -22,11 +22,6 @@ width: $size; height: $size; - /* Center Lottie animation */ - display: flex; - align-items: center; - justify-content: center; - &--muted { @include color-svg( '../images/icons/v2/mic-off-solid-28.svg', diff --git a/ts/calling/constants.ts b/ts/calling/constants.ts index 8de9f53ca..c12ae22af 100644 --- a/ts/calling/constants.ts +++ b/ts/calling/constants.ts @@ -1,8 +1,8 @@ // Copyright 2020-2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -export const AUDIO_LEVEL_INTERVAL_MS = 250; -export const DEFAULT_AUDIO_LEVEL_FOR_SPEAKING = 0.15; +// See `TICK_INTERVAL` in group_call.rs in RingRTC +export const AUDIO_LEVEL_INTERVAL_MS = 200; export const REQUESTED_VIDEO_WIDTH = 640; export const REQUESTED_VIDEO_HEIGHT = 480; diff --git a/ts/calling/getAudioLevelForSpeaking.ts b/ts/calling/getAudioLevelForSpeaking.ts deleted file mode 100644 index 543f0707e..000000000 --- a/ts/calling/getAudioLevelForSpeaking.ts +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2022 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import type * as RemoteConfig from '../RemoteConfig'; -import { DEFAULT_AUDIO_LEVEL_FOR_SPEAKING } from './constants'; - -export function getAudioLevelForSpeaking( - getValueFromRemoteConfig: typeof RemoteConfig.getValue -): number { - const configValue = getValueFromRemoteConfig( - 'desktop.calling.audioLevelForSpeaking' - ); - if (typeof configValue !== 'string') { - return DEFAULT_AUDIO_LEVEL_FOR_SPEAKING; - } - - const result = parseFloat(configValue); - const isResultValid = result > 0 && result <= 1; - return isResultValid ? result : DEFAULT_AUDIO_LEVEL_FOR_SPEAKING; -} diff --git a/ts/calling/truncateAudioLevel.ts b/ts/calling/truncateAudioLevel.ts new file mode 100644 index 000000000..4911b4073 --- /dev/null +++ b/ts/calling/truncateAudioLevel.ts @@ -0,0 +1,24 @@ +// Copyright 2022 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +// See https://github.com/signalapp/Signal-Android/blob/b527b2ffb94fb82a7508c3b33ddbffef28085349/app/src/main/java/org/thoughtcrime/securesms/events/CallParticipant.kt#L87-L100 +const LOWEST = 500 / 32767; +const LOW = 1000 / 32767; +const MEDIUM = 5000 / 32767; +const HIGH = 16000 / 32767; + +export function truncateAudioLevel(audioLevel: number): number { + if (audioLevel < LOWEST) { + return 0; + } + if (audioLevel < LOW) { + return 0.25; + } + if (audioLevel < MEDIUM) { + return 0.5; + } + if (audioLevel < HIGH) { + return 0.75; + } + return 1; +} diff --git a/ts/components/CallManager.stories.tsx b/ts/components/CallManager.stories.tsx index b177c06e8..6741fd83f 100644 --- a/ts/components/CallManager.stories.tsx +++ b/ts/components/CallManager.stories.tsx @@ -50,7 +50,7 @@ const getCommonActiveCallData = () => ({ joinedAt: Date.now(), hasLocalAudio: boolean('hasLocalAudio', true), hasLocalVideo: boolean('hasLocalVideo', false), - amISpeaking: boolean('amISpeaking', false), + localAudioLevel: select('localAudioLevel', [0, 0.5, 1], 0), isInSpeakerView: boolean('isInSpeakerView', false), outgoingRing: boolean('outgoingRing', true), pip: boolean('pip', false), @@ -145,7 +145,7 @@ story.add('Ongoing Group Call', () => ( groupMembers: [], peekedParticipants: [], remoteParticipants: [], - speakingDemuxIds: new Set(), + remoteAudioLevels: new Map(), }, })} /> @@ -220,7 +220,7 @@ story.add('Group call - Safety Number Changed', () => ( groupMembers: [], peekedParticipants: [], remoteParticipants: [], - speakingDemuxIds: new Set(), + remoteAudioLevels: new Map(), }, })} /> diff --git a/ts/components/CallScreen.stories.tsx b/ts/components/CallScreen.stories.tsx index 2e23541d2..01b9a69cf 100644 --- a/ts/components/CallScreen.stories.tsx +++ b/ts/components/CallScreen.stories.tsx @@ -44,7 +44,7 @@ const conversation = getDefaultConversation({ type OverridePropsBase = { hasLocalAudio?: boolean; hasLocalVideo?: boolean; - amISpeaking?: boolean; + localAudioLevel?: number; isInSpeakerView?: boolean; }; @@ -104,7 +104,7 @@ const createActiveGroupCallProp = (overrideProps: GroupCallOverrideProps) => ({ peekedParticipants: overrideProps.peekedParticipants || overrideProps.remoteParticipants || [], remoteParticipants: overrideProps.remoteParticipants || [], - speakingDemuxIds: new Set(), + remoteAudioLevels: new Map(), }); const createActiveCallProp = ( @@ -121,7 +121,11 @@ const createActiveCallProp = ( 'hasLocalVideo', overrideProps.hasLocalVideo || false ), - amISpeaking: boolean('amISpeaking', overrideProps.amISpeaking || false), + localAudioLevel: select( + 'localAudioLevel', + [0, 0.5, 1], + overrideProps.localAudioLevel || 0 + ), isInSpeakerView: boolean( 'isInSpeakerView', overrideProps.isInSpeakerView || false diff --git a/ts/components/CallScreen.tsx b/ts/components/CallScreen.tsx index 68056db97..3a03bf7b0 100644 --- a/ts/components/CallScreen.tsx +++ b/ts/components/CallScreen.tsx @@ -130,7 +130,7 @@ export const CallScreen: React.FC = ({ conversation, hasLocalAudio, hasLocalVideo, - amISpeaking, + localAudioLevel, isInSpeakerView, presentingSource, remoteParticipants, @@ -296,7 +296,7 @@ export const CallScreen: React.FC = ({ isInSpeakerView={isInSpeakerView} remoteParticipants={activeCall.remoteParticipants} setGroupCallVideoRequest={setGroupCallVideoRequest} - speakingDemuxIds={activeCall.speakingDemuxIds} + remoteAudioLevels={activeCall.remoteAudioLevels} /> ); break; @@ -514,7 +514,7 @@ export const CallScreen: React.FC = ({ {localPreviewNode} diff --git a/ts/components/CallingAudioIndicator.stories.tsx b/ts/components/CallingAudioIndicator.stories.tsx new file mode 100644 index 000000000..08a0e0a20 --- /dev/null +++ b/ts/components/CallingAudioIndicator.stories.tsx @@ -0,0 +1,17 @@ +// Copyright 2022 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { boolean, select } from '@storybook/addon-knobs'; + +import { CallingAudioIndicator } from './CallingAudioIndicator'; + +const story = storiesOf('Components/CallingAudioIndicator', module); + +story.add('Default', () => ( + +)); diff --git a/ts/components/CallingAudioIndicator.tsx b/ts/components/CallingAudioIndicator.tsx index 14f7c5099..a35d05507 100644 --- a/ts/components/CallingAudioIndicator.tsx +++ b/ts/components/CallingAudioIndicator.tsx @@ -4,23 +4,112 @@ import classNames from 'classnames'; import { noop } from 'lodash'; import type { ReactElement } from 'react'; -import React, { useEffect, useState } from 'react'; -import animationData from '../../images/lottie-animations/CallingSpeakingIndicator.json'; -import { Lottie } from './Lottie'; +import React, { useEffect, useCallback, useState } from 'react'; +import { useSpring, animated } from '@react-spring/web'; -const SPEAKING_LINGER_MS = 100; +import { AUDIO_LEVEL_INTERVAL_MS } from '../calling/constants'; +import { missingCaseError } from '../util/missingCaseError'; + +const SPEAKING_LINGER_MS = 500; const BASE_CLASS_NAME = 'CallingAudioIndicator'; const CONTENT_CLASS_NAME = `${BASE_CLASS_NAME}__content`; -export function CallingAudioIndicator({ - hasAudio, - isSpeaking, -}: Readonly<{ hasAudio: boolean; isSpeaking: boolean }>): ReactElement { - const [shouldShowSpeaking, setShouldShowSpeaking] = useState(isSpeaking); +const MIN_BAR_HEIGHT = 2; +const SIDE_SCALE_FACTOR = 0.75; +const MAX_CENTRAL_BAR_DELTA = 9; + +/* Should match css */ +const CONTENT_WIDTH = 14; +const CONTENT_HEIGHT = 14; +const BAR_WIDTH = 2; + +const CONTENT_PADDING = 1; + +enum BarPosition { + Left, + Center, + Right, +} + +function generateBarPath(position: BarPosition, audioLevel: number): string { + let x: number; + if (position === BarPosition.Left) { + x = CONTENT_PADDING; + } else if (position === BarPosition.Center) { + x = CONTENT_WIDTH / 2 - CONTENT_PADDING; + } else if (position === BarPosition.Right) { + x = CONTENT_WIDTH - CONTENT_PADDING - BAR_WIDTH; + } else { + throw missingCaseError(position); + } + + let height: number; + if (position === BarPosition.Left || position === BarPosition.Right) { + height = + MIN_BAR_HEIGHT + audioLevel * MAX_CENTRAL_BAR_DELTA * SIDE_SCALE_FACTOR; + } else if (position === BarPosition.Center) { + height = MIN_BAR_HEIGHT + audioLevel * MAX_CENTRAL_BAR_DELTA; + } else { + throw missingCaseError(position); + } + + // Take the round corners off the height + height -= 2; + + const y = (CONTENT_HEIGHT - height) / 2; + const top = y; + const bottom = top + height; + + return ( + `M ${x} ${top} ` + + `L ${x} ${bottom} ` + + `A 0.5 0.5 0 0 0 ${x + BAR_WIDTH} ${bottom} ` + + `L ${x + BAR_WIDTH} ${top} ` + + `A 0.5 0.5 0 0 0 ${x} ${top}` + ); +} + +function Bar({ + position, + audioLevel, +}: { + position: BarPosition; + audioLevel: number; +}): ReactElement { + const animatedProps = useSpring({ + from: { audioLevel: 0 }, + config: { duration: AUDIO_LEVEL_INTERVAL_MS }, + }); + + const levelToPath = useCallback( + (animatedLevel: number): string => { + return generateBarPath(position, animatedLevel); + }, + [position] + ); useEffect(() => { - if (isSpeaking) { + animatedProps.audioLevel.stop(); + animatedProps.audioLevel.start(audioLevel); + }, [audioLevel, animatedProps]); + + return ( + + ); +} + +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(() => { @@ -31,7 +120,7 @@ export function CallingAudioIndicator({ }; } return noop; - }, [isSpeaking, shouldShowSpeaking]); + }, [audioLevel, shouldShowSpeaking]); if (!hasAudio) { return ( @@ -59,7 +148,11 @@ export function CallingAudioIndicator({ `${BASE_CLASS_NAME}--with-content` )} > - + + + + + ); } diff --git a/ts/components/CallingPip.stories.tsx b/ts/components/CallingPip.stories.tsx index 14006ed1a..46b02c198 100644 --- a/ts/components/CallingPip.stories.tsx +++ b/ts/components/CallingPip.stories.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import { times } from 'lodash'; import { storiesOf } from '@storybook/react'; -import { boolean } from '@storybook/addon-knobs'; +import { boolean, select } from '@storybook/addon-knobs'; import { action } from '@storybook/addon-actions'; import { AvatarColors } from '../types/Colors'; @@ -39,7 +39,7 @@ const getCommonActiveCallData = () => ({ conversation, hasLocalAudio: boolean('hasLocalAudio', true), hasLocalVideo: boolean('hasLocalVideo', false), - amISpeaking: boolean('amISpeaking', false), + localAudioLevel: select('localAudioLevel', [0, 0.5, 1], 0), isInSpeakerView: boolean('isInSpeakerView', false), joinedAt: Date.now(), outgoingRing: true, @@ -120,7 +120,7 @@ story.add('Group Call', () => { deviceCount: 0, peekedParticipants: [], remoteParticipants: [], - speakingDemuxIds: new Set(), + remoteAudioLevels: new Map(), }, }); return ; diff --git a/ts/components/GroupCallOverflowArea.stories.tsx b/ts/components/GroupCallOverflowArea.stories.tsx index 8fb5abf64..68a2184ed 100644 --- a/ts/components/GroupCallOverflowArea.stories.tsx +++ b/ts/components/GroupCallOverflowArea.stories.tsx @@ -39,7 +39,7 @@ const defaultProps = { getGroupCallVideoFrameSource: fakeGetGroupCallVideoFrameSource, i18n, onParticipantVisibilityChanged: action('onParticipantVisibilityChanged'), - speakingDemuxIds: new Set(), + remoteAudioLevels: new Map(), }; // This component is usually rendered on a call screen. diff --git a/ts/components/GroupCallOverflowArea.tsx b/ts/components/GroupCallOverflowArea.tsx index 55290fde9..0bff0201e 100644 --- a/ts/components/GroupCallOverflowArea.tsx +++ b/ts/components/GroupCallOverflowArea.tsx @@ -24,7 +24,7 @@ type PropsType = { isVisible: boolean ) => unknown; overflowedParticipants: ReadonlyArray; - speakingDemuxIds: Set; + remoteAudioLevels: Map; }; export const GroupCallOverflowArea: FC = ({ @@ -33,7 +33,7 @@ export const GroupCallOverflowArea: FC = ({ i18n, onParticipantVisibilityChanged, overflowedParticipants, - speakingDemuxIds, + remoteAudioLevels, }) => { const overflowRef = useRef(null); const [overflowScrollTop, setOverflowScrollTop] = useState(0); @@ -116,7 +116,7 @@ export const GroupCallOverflowArea: FC = ({ getFrameBuffer={getFrameBuffer} getGroupCallVideoFrameSource={getGroupCallVideoFrameSource} i18n={i18n} - isSpeaking={speakingDemuxIds.has(remoteParticipant.demuxId)} + audioLevel={remoteAudioLevels.get(remoteParticipant.demuxId) ?? 0} onVisibilityChanged={onParticipantVisibilityChanged} width={OVERFLOW_PARTICIPANT_WIDTH} height={Math.floor( diff --git a/ts/components/GroupCallRemoteParticipant.stories.tsx b/ts/components/GroupCallRemoteParticipant.stories.tsx index 2dc516617..b24dd142c 100644 --- a/ts/components/GroupCallRemoteParticipant.stories.tsx +++ b/ts/components/GroupCallRemoteParticipant.stories.tsx @@ -4,6 +4,7 @@ import * as React from 'react'; import { memoize, noop } from 'lodash'; import { storiesOf } from '@storybook/react'; +import { select } from '@storybook/addon-knobs'; import type { PropsType } from './GroupCallRemoteParticipant'; import { GroupCallRemoteParticipant } from './GroupCallRemoteParticipant'; @@ -14,7 +15,9 @@ import enMessages from '../../_locales/en/messages.json'; const i18n = setupI18n('en', enMessages); -type OverridePropsType = +type OverridePropsType = { + audioLevel?: number; +} & ( | { isInPip: true; } @@ -24,22 +27,29 @@ type OverridePropsType = left: number; top: number; width: number; - }; + } +); const getFrameBuffer = memoize(() => Buffer.alloc(FRAME_BUFFER_SIZE)); const createProps = ( overrideProps: OverridePropsType, - isBlocked?: boolean + { + isBlocked = false, + hasRemoteAudio = false, + }: { + isBlocked?: boolean; + hasRemoteAudio?: boolean; + } = {} ): PropsType => ({ getFrameBuffer, // eslint-disable-next-line @typescript-eslint/no-explicit-any getGroupCallVideoFrameSource: noop as any, i18n, - isSpeaking: false, + audioLevel: 0, remoteParticipant: { demuxId: 123, - hasRemoteAudio: false, + hasRemoteAudio, hasRemoteVideo: true, presenting: false, sharingScreen: false, @@ -52,7 +62,6 @@ const createProps = ( }), }, ...overrideProps, - ...(overrideProps.isInPip ? {} : { isSpeaking: false }), }); const story = storiesOf('Components/GroupCallRemoteParticipant', module); @@ -69,6 +78,22 @@ story.add('Default', () => ( /> )); +story.add('Speaking', () => ( + +)); + story.add('isInPip', () => ( ( top: 0, width: 120, }, - true + { isBlocked: true } )} /> )); diff --git a/ts/components/GroupCallRemoteParticipant.tsx b/ts/components/GroupCallRemoteParticipant.tsx index 2ad4981cd..4c2421eba 100644 --- a/ts/components/GroupCallRemoteParticipant.tsx +++ b/ts/components/GroupCallRemoteParticipant.tsx @@ -42,7 +42,7 @@ type InPipPropsType = { type InOverflowAreaPropsType = { height: number; isInPip?: false; - isSpeaking: boolean; + audioLevel: number; width: number; }; @@ -282,7 +282,7 @@ export const GroupCallRemoteParticipant: React.FC = React.memo( /> )} diff --git a/ts/components/GroupCallRemoteParticipants.tsx b/ts/components/GroupCallRemoteParticipants.tsx index 7f145fba5..0b045d27e 100644 --- a/ts/components/GroupCallRemoteParticipants.tsx +++ b/ts/components/GroupCallRemoteParticipants.tsx @@ -48,7 +48,7 @@ type PropsType = { isInSpeakerView: boolean; remoteParticipants: ReadonlyArray; setGroupCallVideoRequest: (_: Array) => void; - speakingDemuxIds: Set; + remoteAudioLevels: Map; }; enum VideoRequestMode { @@ -86,7 +86,7 @@ export const GroupCallRemoteParticipants: React.FC = ({ isInSpeakerView, remoteParticipants, setGroupCallVideoRequest, - speakingDemuxIds, + remoteAudioLevels, }) => { const [containerDimensions, setContainerDimensions] = useState({ width: 0, @@ -270,7 +270,7 @@ export const GroupCallRemoteParticipants: React.FC = ({ return remoteParticipantsInRow.map(remoteParticipant => { const { demuxId, videoAspectRatio } = remoteParticipant; - const isSpeaking = speakingDemuxIds.has(demuxId); + const audioLevel = remoteAudioLevels.get(demuxId) ?? 0; const renderedWidth = Math.floor( videoAspectRatio * gridParticipantHeight @@ -286,7 +286,7 @@ export const GroupCallRemoteParticipants: React.FC = ({ getGroupCallVideoFrameSource={getGroupCallVideoFrameSource} height={gridParticipantHeight} i18n={i18n} - isSpeaking={isSpeaking} + audioLevel={audioLevel} left={left} remoteParticipant={remoteParticipant} top={top} @@ -418,7 +418,7 @@ export const GroupCallRemoteParticipants: React.FC = ({ i18n={i18n} onParticipantVisibilityChanged={onParticipantVisibilityChanged} overflowedParticipants={overflowedParticipants} - speakingDemuxIds={speakingDemuxIds} + remoteAudioLevels={remoteAudioLevels} /> )} diff --git a/ts/services/calling.ts b/ts/services/calling.ts index 2d2bdc094..b49258cf8 100644 --- a/ts/services/calling.ts +++ b/ts/services/calling.ts @@ -37,7 +37,6 @@ import { } from 'ringrtc'; import { uniqBy, noop } from 'lodash'; -import * as RemoteConfig from '../RemoteConfig'; import type { ActionsType as UxActionsType, GroupCallParticipantInfoType, @@ -63,7 +62,6 @@ import { getAudioDeviceModule, parseAudioDeviceModule, } from '../calling/audioDeviceModule'; -import { getAudioLevelForSpeaking } from '../calling/getAudioLevelForSpeaking'; import { findBestMatchingAudioDeviceIndex, findBestMatchingCameraId, @@ -684,9 +682,6 @@ export class CallingClass { const localAudioLevel = groupCall.getLocalDeviceState().audioLevel; this.uxActions?.groupCallAudioLevelsChange({ - audioLevelForSpeaking: getAudioLevelForSpeaking( - RemoteConfig.getValue - ), conversationId, localAudioLevel, remoteDeviceStates, diff --git a/ts/state/ducks/calling.ts b/ts/state/ducks/calling.ts index 80470ee75..8b314dae9 100644 --- a/ts/state/ducks/calling.ts +++ b/ts/state/ducks/calling.ts @@ -15,6 +15,7 @@ import { getPlatform } from '../selectors/user'; import { isConversationTooBigToRing } from '../../conversations/isConversationTooBigToRing'; import { missingCaseError } from '../../util/missingCaseError'; import { calling } from '../../services/calling'; +import { truncateAudioLevel } from '../../calling/truncateAudioLevel'; import type { StateType as RootStateType } from '../reducer'; import type { ChangeIODevicePayloadType, @@ -44,7 +45,7 @@ import { getConversationCallMode } from './conversations'; import * as log from '../../logging/log'; import { strictAssert } from '../../util/assert'; import { waitForOnline } from '../../util/waitForOnline'; -import * as setUtil from '../../util/setUtil'; +import * as mapUtil from '../../util/mapUtil'; // State @@ -95,14 +96,14 @@ export type GroupCallStateType = { joinState: GroupCallJoinState; peekInfo?: GroupCallPeekInfoType; remoteParticipants: Array; - speakingDemuxIds?: Set; + remoteAudioLevels?: Map; } & GroupCallRingStateType; export type ActiveCallStateType = { conversationId: string; hasLocalAudio: boolean; hasLocalVideo: boolean; - amISpeaking: boolean; + localAudioLevel: number; isInSpeakerView: boolean; joinedAt?: number; outgoingRing: boolean; @@ -467,7 +468,6 @@ type DeclineCallActionType = { }; type GroupCallAudioLevelsChangeActionPayloadType = Readonly<{ - audioLevelForSpeaking: number; conversationId: string; localAudioLevel: number; remoteDeviceStates: ReadonlyArray<{ audioLevel: number; demuxId: number }>; @@ -1456,7 +1456,7 @@ export function reducer( conversationId: action.payload.conversationId, hasLocalAudio: action.payload.hasLocalAudio, hasLocalVideo: action.payload.hasLocalVideo, - amISpeaking: false, + localAudioLevel: 0, isInSpeakerView: false, pip: false, safetyNumberChangedUuids: [], @@ -1484,7 +1484,7 @@ export function reducer( conversationId: action.payload.conversationId, hasLocalAudio: action.payload.hasLocalAudio, hasLocalVideo: action.payload.hasLocalVideo, - amISpeaking: false, + localAudioLevel: 0, isInSpeakerView: false, pip: false, safetyNumberChangedUuids: [], @@ -1507,7 +1507,7 @@ export function reducer( conversationId: action.payload.conversationId, hasLocalAudio: true, hasLocalVideo: action.payload.asVideoCall, - amISpeaking: false, + localAudioLevel: 0, isInSpeakerView: false, pip: false, safetyNumberChangedUuids: [], @@ -1661,7 +1661,7 @@ export function reducer( conversationId: action.payload.conversationId, hasLocalAudio: action.payload.hasLocalAudio, hasLocalVideo: action.payload.hasLocalVideo, - amISpeaking: false, + localAudioLevel: 0, isInSpeakerView: false, pip: false, safetyNumberChangedUuids: [], @@ -1719,12 +1719,7 @@ export function reducer( } if (action.type === GROUP_CALL_AUDIO_LEVELS_CHANGE) { - const { - audioLevelForSpeaking, - conversationId, - localAudioLevel, - remoteDeviceStates, - } = action.payload; + const { conversationId, remoteDeviceStates } = action.payload; const { activeCallState } = state; const existingCall = getGroupCall(conversationId, state); @@ -1735,36 +1730,38 @@ export function reducer( return state; } - const amISpeaking = localAudioLevel > audioLevelForSpeaking; + const localAudioLevel = truncateAudioLevel(action.payload.localAudioLevel); - const speakingDemuxIds = new Set(); + const remoteAudioLevels = new Map(); remoteDeviceStates.forEach(({ audioLevel, demuxId }) => { // We expect `audioLevel` to be a number but have this check just in case. - if ( - typeof audioLevel === 'number' && - audioLevel > audioLevelForSpeaking - ) { - speakingDemuxIds.add(demuxId); + if (typeof audioLevel !== 'number') { + return; + } + + const graded = truncateAudioLevel(audioLevel); + if (graded > 0) { + remoteAudioLevels.set(demuxId, graded); } }); // This action is dispatched frequently. This equality check helps avoid re-renders. - const oldAmISpeaking = activeCallState.amISpeaking; - const oldSpeakingDemuxIds = existingCall.speakingDemuxIds; + const oldLocalAudioLevel = activeCallState.localAudioLevel; + const oldRemoteAudioLevels = existingCall.remoteAudioLevels; if ( - oldAmISpeaking === amISpeaking && - oldSpeakingDemuxIds && - setUtil.isEqual(oldSpeakingDemuxIds, speakingDemuxIds) + oldLocalAudioLevel === localAudioLevel && + oldRemoteAudioLevels && + mapUtil.isEqual(oldRemoteAudioLevels, remoteAudioLevels) ) { return state; } return { ...state, - activeCallState: { ...activeCallState, amISpeaking }, + activeCallState: { ...activeCallState, localAudioLevel }, callsByConversation: { ...callsByConversation, - [conversationId]: { ...existingCall, speakingDemuxIds }, + [conversationId]: { ...existingCall, remoteAudioLevels }, }, }; } diff --git a/ts/state/smart/CallManager.tsx b/ts/state/smart/CallManager.tsx index d6e0eac8e..dcdd78aa1 100644 --- a/ts/state/smart/CallManager.tsx +++ b/ts/state/smart/CallManager.tsx @@ -130,7 +130,7 @@ const mapStateToActiveCallProp = ( conversation, hasLocalAudio: activeCallState.hasLocalAudio, hasLocalVideo: activeCallState.hasLocalVideo, - amISpeaking: activeCallState.amISpeaking, + localAudioLevel: activeCallState.localAudioLevel, isInSpeakerView: activeCallState.isInSpeakerView, joinedAt: activeCallState.joinedAt, outgoingRing: activeCallState.outgoingRing, @@ -263,7 +263,7 @@ const mapStateToActiveCallProp = ( maxDevices: peekInfo.maxDevices, peekedParticipants, remoteParticipants, - speakingDemuxIds: call.speakingDemuxIds || new Set(), + remoteAudioLevels: call.remoteAudioLevels || new Map(), }; } default: diff --git a/ts/test-both/util/mapUtil_test.ts b/ts/test-both/util/mapUtil_test.ts index bad6d44a5..6d7b091b0 100644 --- a/ts/test-both/util/mapUtil_test.ts +++ b/ts/test-both/util/mapUtil_test.ts @@ -4,7 +4,7 @@ import { assert } from 'chai'; import * as sinon from 'sinon'; -import { groupBy } from '../../util/mapUtil'; +import { groupBy, isEqual } from '../../util/mapUtil'; describe('map utilities', () => { describe('groupBy', () => { @@ -27,4 +27,45 @@ describe('map utilities', () => { ); }); }); + + describe('isEqual', () => { + it('returns false on different maps', () => { + assert.isFalse( + isEqual(new Map([]), new Map([['key', 1]])) + ); + + assert.isFalse( + isEqual(new Map([['key', 0]]), new Map([['key', 1]])) + ); + + assert.isFalse( + isEqual( + new Map([ + ['key', 1], + ['another-key', 2], + ]), + new Map([['key', 1]]) + ) + ); + }); + + it('returns true on equal maps', () => { + assert.isTrue(isEqual(new Map([]), new Map([]))); + assert.isTrue( + isEqual(new Map([['key', 1]]), new Map([['key', 1]])) + ); + assert.isTrue( + isEqual( + new Map([ + ['a', 1], + ['b', 2], + ]), + new Map([ + ['b', 2], + ['a', 1], + ]) + ) + ); + }); + }); }); diff --git a/ts/test-electron/state/ducks/calling_test.ts b/ts/test-electron/state/ducks/calling_test.ts index e738603db..80d0a4af7 100644 --- a/ts/test-electron/state/ducks/calling_test.ts +++ b/ts/test-electron/state/ducks/calling_test.ts @@ -18,6 +18,7 @@ import { isAnybodyElseInGroupCall, reducer, } from '../../../state/ducks/calling'; +import { truncateAudioLevel } from '../../../calling/truncateAudioLevel'; import { calling as callingService } from '../../../services/calling'; import { CallMode, @@ -50,7 +51,7 @@ describe('calling duck', () => { conversationId: 'fake-direct-call-conversation-id', hasLocalAudio: true, hasLocalVideo: false, - amISpeaking: false, + localAudioLevel: 0, isInSpeakerView: false, showParticipantsList: false, safetyNumberChangedUuids: [], @@ -129,7 +130,7 @@ describe('calling duck', () => { conversationId: 'fake-group-call-conversation-id', hasLocalAudio: true, hasLocalVideo: false, - amISpeaking: false, + localAudioLevel: 0, isInSpeakerView: false, showParticipantsList: false, safetyNumberChangedUuids: [], @@ -435,7 +436,7 @@ describe('calling duck', () => { conversationId: 'fake-direct-call-conversation-id', hasLocalAudio: true, hasLocalVideo: true, - amISpeaking: false, + localAudioLevel: 0, isInSpeakerView: false, showParticipantsList: false, safetyNumberChangedUuids: [], @@ -528,7 +529,7 @@ describe('calling duck', () => { conversationId: 'fake-group-call-conversation-id', hasLocalAudio: true, hasLocalVideo: true, - amISpeaking: false, + localAudioLevel: 0, isInSpeakerView: false, showParticipantsList: false, safetyNumberChangedUuids: [], @@ -763,9 +764,16 @@ describe('calling duck', () => { { audioLevel: 0, demuxId: 9 }, ]; + const remoteAudioLevels = new Map([ + [1, truncateAudioLevel(0.3)], + [2, truncateAudioLevel(0.4)], + [3, truncateAudioLevel(0.5)], + [7, truncateAudioLevel(0.2)], + [8, truncateAudioLevel(0.1)], + ]); + it("does nothing if there's no relevant call", () => { const action = groupCallAudioLevelsChange({ - audioLevelForSpeaking: 0.25, conversationId: 'garbage', localAudioLevel: 1, remoteDeviceStates, @@ -784,14 +792,13 @@ describe('calling duck', () => { ...stateWithActiveGroupCall.callsByConversation[ 'fake-group-call-conversation-id' ], - speakingDemuxIds: new Set([3, 2, 1]), + remoteAudioLevels, }, }, }; const action = groupCallAudioLevelsChange({ - audioLevelForSpeaking: 0.25, conversationId: 'fake-group-call-conversation-id', - localAudioLevel: 0.1, + localAudioLevel: 0.001, remoteDeviceStates, }); @@ -802,21 +809,23 @@ describe('calling duck', () => { it('updates the set of speaking participants, including yourself', () => { const action = groupCallAudioLevelsChange({ - audioLevelForSpeaking: 0.25, conversationId: 'fake-group-call-conversation-id', localAudioLevel: 0.8, remoteDeviceStates, }); const result = reducer(stateWithActiveGroupCall, action); - assert.isTrue(result.activeCallState?.amISpeaking); + assert.strictEqual( + result.activeCallState?.localAudioLevel, + truncateAudioLevel(0.8) + ); const call = result.callsByConversation['fake-group-call-conversation-id']; if (call?.callMode !== CallMode.Group) { throw new Error('Expected a group call to be found'); } - assert.deepStrictEqual(call.speakingDemuxIds, new Set([1, 2, 3])); + assert.deepStrictEqual(call.remoteAudioLevels, remoteAudioLevels); }); }); @@ -1112,7 +1121,7 @@ describe('calling duck', () => { conversationId: 'fake-group-call-conversation-id', hasLocalAudio: true, hasLocalVideo: false, - amISpeaking: false, + localAudioLevel: 0, isInSpeakerView: false, showParticipantsList: false, safetyNumberChangedUuids: [], @@ -1641,7 +1650,7 @@ describe('calling duck', () => { conversationId: 'fake-conversation-id', hasLocalAudio: true, hasLocalVideo: true, - amISpeaking: false, + localAudioLevel: 0, isInSpeakerView: false, showParticipantsList: false, safetyNumberChangedUuids: [], @@ -1927,7 +1936,7 @@ describe('calling duck', () => { conversationId: 'fake-conversation-id', hasLocalAudio: true, hasLocalVideo: false, - amISpeaking: false, + localAudioLevel: 0, isInSpeakerView: false, showParticipantsList: false, safetyNumberChangedUuids: [], diff --git a/ts/test-electron/state/selectors/calling_test.ts b/ts/test-electron/state/selectors/calling_test.ts index 16ce8d374..4c29d71b2 100644 --- a/ts/test-electron/state/selectors/calling_test.ts +++ b/ts/test-electron/state/selectors/calling_test.ts @@ -51,7 +51,7 @@ describe('state/selectors/calling', () => { conversationId: 'fake-direct-call-conversation-id', hasLocalAudio: true, hasLocalVideo: false, - amISpeaking: false, + localAudioLevel: 0, isInSpeakerView: false, showParticipantsList: false, safetyNumberChangedUuids: [], diff --git a/ts/types/Calling.ts b/ts/types/Calling.ts index e16232da0..2239274bd 100644 --- a/ts/types/Calling.ts +++ b/ts/types/Calling.ts @@ -28,7 +28,7 @@ type ActiveCallBaseType = { conversation: ConversationType; hasLocalAudio: boolean; hasLocalVideo: boolean; - amISpeaking: boolean; + localAudioLevel: number; isInSpeakerView: boolean; isSharingScreen?: boolean; joinedAt?: number; @@ -66,7 +66,7 @@ type ActiveGroupCallType = ActiveCallBaseType & { groupMembers: Array>; peekedParticipants: Array; remoteParticipants: Array; - speakingDemuxIds: Set; + remoteAudioLevels: Map; }; export type ActiveCallType = ActiveDirectCallType | ActiveGroupCallType; diff --git a/ts/util/mapUtil.ts b/ts/util/mapUtil.ts index 38dc0019e..8b6c86d24 100644 --- a/ts/util/mapUtil.ts +++ b/ts/util/mapUtil.ts @@ -24,3 +24,24 @@ export const groupBy = ( }, new Map>() ); + +export const isEqual = ( + left: ReadonlyMap, + right: ReadonlyMap +): boolean => { + if (left.size !== right.size) { + return false; + } + + for (const [key, value] of left) { + if (!right.has(key)) { + return false; + } + + if (right.get(key) !== value) { + return false; + } + } + + return true; +};