diff --git a/package.json b/package.json index 6f65317ad..fa82924fc 100644 --- a/package.json +++ b/package.json @@ -135,7 +135,7 @@ "redux-ts-utils": "3.2.2", "reselect": "4.0.0", "rimraf": "2.6.2", - "ringrtc": "https://github.com/signalapp/signal-ringrtc-node.git#e0c31ca67271850c736c10786139c65564330a73", + "ringrtc": "https://github.com/signalapp/signal-ringrtc-node.git#eb01373e3279aab7ed3b718458af1cc5c45df63c", "sanitize-filename": "1.6.3", "sanitize.css": "11.0.0", "semver": "5.4.1", diff --git a/ts/components/CallingPipRemoteVideo.tsx b/ts/components/CallingPipRemoteVideo.tsx index cac60a8b2..07c03828f 100644 --- a/ts/components/CallingPipRemoteVideo.tsx +++ b/ts/components/CallingPipRemoteVideo.tsx @@ -1,13 +1,18 @@ // Copyright 2020 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import React from 'react'; +import React, { useMemo } from 'react'; +import { maxBy } from 'lodash'; import { Avatar } from './Avatar'; import { CallBackgroundBlur } from './CallBackgroundBlur'; import { DirectCallRemoteParticipant } from './DirectCallRemoteParticipant'; import { GroupCallRemoteParticipant } from './GroupCallRemoteParticipant'; import { LocalizerType } from '../types/Util'; -import { CallMode, VideoFrameSource } from '../types/Calling'; +import { + CallMode, + GroupCallRemoteParticipantType, + VideoFrameSource, +} from '../types/Calling'; import { ActiveCallType, SetRendererCanvasType } from '../state/ducks/calling'; const NoVideo = ({ @@ -61,7 +66,20 @@ export const CallingPipRemoteVideo = ({ i18n, setRendererCanvas, }: PropsType): JSX.Element => { - const { call, conversation } = activeCall; + const { call, conversation, groupCallParticipants } = activeCall; + + const activeGroupCallSpeaker: + | undefined + | GroupCallRemoteParticipantType = useMemo(() => { + if (call.callMode !== CallMode.Group) { + return undefined; + } + + return maxBy( + groupCallParticipants, + participant => participant.speakerTime || -Infinity + ); + }, [call.callMode, groupCallParticipants]); if (call.callMode === CallMode.Direct) { if (!call.hasRemoteVideo) { @@ -81,10 +99,7 @@ export const CallingPipRemoteVideo = ({ } if (call.callMode === CallMode.Group) { - const { groupCallParticipants } = activeCall; - const speaker = groupCallParticipants[0]; - - if (!speaker) { + if (!activeGroupCallSpeaker) { return ; } @@ -94,8 +109,7 @@ export const CallingPipRemoteVideo = ({ getGroupCallVideoFrameSource={getGroupCallVideoFrameSource} i18n={i18n} isInPip - key={speaker.demuxId} - remoteParticipant={speaker} + remoteParticipant={activeGroupCallSpeaker} /> ); diff --git a/ts/services/calling.ts b/ts/services/calling.ts index 20d48b0bc..021c82d57 100644 --- a/ts/services/calling.ts +++ b/ts/services/calling.ts @@ -52,6 +52,7 @@ import { import { getOwn } from '../util/getOwn'; import { fetchMembershipProof, getMembershipList } from '../groups'; import { missingCaseError } from '../util/missingCaseError'; +import { normalizeGroupCallTimestamp } from '../util/ringrtc/normalizeGroupCallTimestamp'; const RINGRTC_SFU_URL = 'https://sfu.voip.signal.org/'; @@ -598,6 +599,9 @@ export class CallingClass { hasRemoteAudio: !remoteDeviceState.audioMuted, hasRemoteVideo: !remoteDeviceState.videoMuted, isSelf: conversationId === ourConversationId, + speakerTime: normalizeGroupCallTimestamp( + remoteDeviceState.speakerTime + ), // If RingRTC doesn't send us an aspect ratio, we make a guess. videoAspectRatio: remoteDeviceState.videoAspectRatio || diff --git a/ts/state/ducks/calling.ts b/ts/state/ducks/calling.ts index f8db1a9e3..5bec9c48f 100644 --- a/ts/state/ducks/calling.ts +++ b/ts/state/ducks/calling.ts @@ -46,6 +46,7 @@ export interface GroupCallParticipantInfoType { hasRemoteAudio: boolean; hasRemoteVideo: boolean; isSelf: boolean; + speakerTime?: number; videoAspectRatio: number; } diff --git a/ts/state/smart/CallManager.tsx b/ts/state/smart/CallManager.tsx index 6cc96f569..f75f2ce58 100644 --- a/ts/state/smart/CallManager.tsx +++ b/ts/state/smart/CallManager.tsx @@ -102,6 +102,7 @@ const mapStateToActiveCallProp = (state: StateType) => { isSelf: remoteParticipant.isSelf, name: remoteConversation.name, profileName: remoteConversation.profileName, + speakerTime: remoteParticipant.speakerTime, title: remoteConversation.title, videoAspectRatio: remoteParticipant.videoAspectRatio, }); diff --git a/ts/test/util/ringrtc/normalizeGroupCallTimestamp_test.ts b/ts/test/util/ringrtc/normalizeGroupCallTimestamp_test.ts new file mode 100644 index 000000000..5a549b940 --- /dev/null +++ b/ts/test/util/ringrtc/normalizeGroupCallTimestamp_test.ts @@ -0,0 +1,74 @@ +// Copyright 2020 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { assert } from 'chai'; + +import { normalizeGroupCallTimestamp } from '../../../util/ringrtc/normalizeGroupCallTimestamp'; + +describe('normalizeGroupCallTimestamp', () => { + it('returns undefined if passed NaN', () => { + assert.isUndefined(normalizeGroupCallTimestamp(NaN)); + }); + + it('returns undefined if passed 0', () => { + assert.isUndefined(normalizeGroupCallTimestamp(0)); + assert.isUndefined(normalizeGroupCallTimestamp(-0)); + }); + + it('returns undefined if passed a negative number', () => { + assert.isUndefined(normalizeGroupCallTimestamp(-1)); + assert.isUndefined(normalizeGroupCallTimestamp(-123)); + }); + + it('returns undefined if passed a string that cannot be parsed as a number', () => { + assert.isUndefined(normalizeGroupCallTimestamp('')); + assert.isUndefined(normalizeGroupCallTimestamp('uhhh')); + }); + + it('returns undefined if passed a BigInt of 0', () => { + assert.isUndefined(normalizeGroupCallTimestamp(BigInt(0))); + }); + + it('returns undefined if passed a negative BigInt', () => { + assert.isUndefined(normalizeGroupCallTimestamp(BigInt(-1))); + assert.isUndefined(normalizeGroupCallTimestamp(BigInt(-123))); + }); + + it('returns undefined if passed a non-parseable type', () => { + [ + undefined, + null, + {}, + [], + [123], + Symbol('123'), + { [Symbol.toPrimitive]: () => 123 }, + // eslint-disable-next-line no-new-wrappers + new Number(123), + ].forEach(value => { + assert.isUndefined(normalizeGroupCallTimestamp(value)); + }); + }); + + it('returns positive numbers passed in', () => { + assert.strictEqual(normalizeGroupCallTimestamp(1), 1); + assert.strictEqual(normalizeGroupCallTimestamp(123), 123); + }); + + it('parses strings as numbers', () => { + assert.strictEqual(normalizeGroupCallTimestamp('1'), 1); + assert.strictEqual(normalizeGroupCallTimestamp('123'), 123); + }); + + it('only parses the first 15 characters of a string', () => { + assert.strictEqual( + normalizeGroupCallTimestamp('12345678901234567890123456789'), + 123456789012345 + ); + }); + + it('converts positive BigInts to numbers', () => { + assert.strictEqual(normalizeGroupCallTimestamp(BigInt(1)), 1); + assert.strictEqual(normalizeGroupCallTimestamp(BigInt(123)), 123); + }); +}); diff --git a/ts/types/Calling.ts b/ts/types/Calling.ts index 5aad000cc..0269d0b00 100644 --- a/ts/types/Calling.ts +++ b/ts/types/Calling.ts @@ -78,6 +78,7 @@ export interface GroupCallRemoteParticipantType { isSelf: boolean; name?: string; profileName?: string; + speakerTime?: number; title: string; videoAspectRatio: number; } diff --git a/ts/util/ringrtc/normalizeGroupCallTimestamp.ts b/ts/util/ringrtc/normalizeGroupCallTimestamp.ts new file mode 100644 index 000000000..2bf0f44c5 --- /dev/null +++ b/ts/util/ringrtc/normalizeGroupCallTimestamp.ts @@ -0,0 +1,36 @@ +// Copyright 2020 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +/** + * Normalizes group call timestamps (`addedTime` and `speakerTime`) into numbers. We + * expect RingRTC to send a string, but it sends a malformed number as of this writing, + * RingRTC 2.8.3. + * + * We could probably safely do `Number(fromRingRtc)` and be done, but this is extra- + * careful. + */ +export function normalizeGroupCallTimestamp( + fromRingRtc: unknown +): undefined | number { + let asNumber: number; + + switch (typeof fromRingRtc) { + case 'number': + asNumber = fromRingRtc; + break; + case 'string': + asNumber = parseInt(fromRingRtc.slice(0, 15), 10); + break; + case 'bigint': + asNumber = Number(fromRingRtc); + break; + default: + return undefined; + } + + if (Number.isNaN(asNumber) || asNumber <= 0) { + return undefined; + } + + return asNumber; +} diff --git a/yarn.lock b/yarn.lock index a4bc12e7a..9daea060c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14293,9 +14293,9 @@ rimraf@~2.4.0: dependencies: glob "^6.0.1" -"ringrtc@https://github.com/signalapp/signal-ringrtc-node.git#e0c31ca67271850c736c10786139c65564330a73": - version "2.8.3" - resolved "https://github.com/signalapp/signal-ringrtc-node.git#e0c31ca67271850c736c10786139c65564330a73" +"ringrtc@https://github.com/signalapp/signal-ringrtc-node.git#eb01373e3279aab7ed3b718458af1cc5c45df63c": + version "2.8.4" + resolved "https://github.com/signalapp/signal-ringrtc-node.git#eb01373e3279aab7ed3b718458af1cc5c45df63c" ripemd160@^2.0.0, ripemd160@^2.0.1: version "2.0.1"