Show active speaker in group calling PiP
This commit is contained in:
parent
b3c161f484
commit
f3f2cb2b5e
9 changed files with 144 additions and 13 deletions
|
@ -135,7 +135,7 @@
|
||||||
"redux-ts-utils": "3.2.2",
|
"redux-ts-utils": "3.2.2",
|
||||||
"reselect": "4.0.0",
|
"reselect": "4.0.0",
|
||||||
"rimraf": "2.6.2",
|
"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-filename": "1.6.3",
|
||||||
"sanitize.css": "11.0.0",
|
"sanitize.css": "11.0.0",
|
||||||
"semver": "5.4.1",
|
"semver": "5.4.1",
|
||||||
|
|
|
@ -1,13 +1,18 @@
|
||||||
// Copyright 2020 Signal Messenger, LLC
|
// Copyright 2020 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// 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 { Avatar } from './Avatar';
|
||||||
import { CallBackgroundBlur } from './CallBackgroundBlur';
|
import { CallBackgroundBlur } from './CallBackgroundBlur';
|
||||||
import { DirectCallRemoteParticipant } from './DirectCallRemoteParticipant';
|
import { DirectCallRemoteParticipant } from './DirectCallRemoteParticipant';
|
||||||
import { GroupCallRemoteParticipant } from './GroupCallRemoteParticipant';
|
import { GroupCallRemoteParticipant } from './GroupCallRemoteParticipant';
|
||||||
import { LocalizerType } from '../types/Util';
|
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';
|
import { ActiveCallType, SetRendererCanvasType } from '../state/ducks/calling';
|
||||||
|
|
||||||
const NoVideo = ({
|
const NoVideo = ({
|
||||||
|
@ -61,7 +66,20 @@ export const CallingPipRemoteVideo = ({
|
||||||
i18n,
|
i18n,
|
||||||
setRendererCanvas,
|
setRendererCanvas,
|
||||||
}: PropsType): JSX.Element => {
|
}: 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.callMode === CallMode.Direct) {
|
||||||
if (!call.hasRemoteVideo) {
|
if (!call.hasRemoteVideo) {
|
||||||
|
@ -81,10 +99,7 @@ export const CallingPipRemoteVideo = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (call.callMode === CallMode.Group) {
|
if (call.callMode === CallMode.Group) {
|
||||||
const { groupCallParticipants } = activeCall;
|
if (!activeGroupCallSpeaker) {
|
||||||
const speaker = groupCallParticipants[0];
|
|
||||||
|
|
||||||
if (!speaker) {
|
|
||||||
return <NoVideo activeCall={activeCall} i18n={i18n} />;
|
return <NoVideo activeCall={activeCall} i18n={i18n} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,8 +109,7 @@ export const CallingPipRemoteVideo = ({
|
||||||
getGroupCallVideoFrameSource={getGroupCallVideoFrameSource}
|
getGroupCallVideoFrameSource={getGroupCallVideoFrameSource}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
isInPip
|
isInPip
|
||||||
key={speaker.demuxId}
|
remoteParticipant={activeGroupCallSpeaker}
|
||||||
remoteParticipant={speaker}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -52,6 +52,7 @@ import {
|
||||||
import { getOwn } from '../util/getOwn';
|
import { getOwn } from '../util/getOwn';
|
||||||
import { fetchMembershipProof, getMembershipList } from '../groups';
|
import { fetchMembershipProof, getMembershipList } from '../groups';
|
||||||
import { missingCaseError } from '../util/missingCaseError';
|
import { missingCaseError } from '../util/missingCaseError';
|
||||||
|
import { normalizeGroupCallTimestamp } from '../util/ringrtc/normalizeGroupCallTimestamp';
|
||||||
|
|
||||||
const RINGRTC_SFU_URL = 'https://sfu.voip.signal.org/';
|
const RINGRTC_SFU_URL = 'https://sfu.voip.signal.org/';
|
||||||
|
|
||||||
|
@ -598,6 +599,9 @@ export class CallingClass {
|
||||||
hasRemoteAudio: !remoteDeviceState.audioMuted,
|
hasRemoteAudio: !remoteDeviceState.audioMuted,
|
||||||
hasRemoteVideo: !remoteDeviceState.videoMuted,
|
hasRemoteVideo: !remoteDeviceState.videoMuted,
|
||||||
isSelf: conversationId === ourConversationId,
|
isSelf: conversationId === ourConversationId,
|
||||||
|
speakerTime: normalizeGroupCallTimestamp(
|
||||||
|
remoteDeviceState.speakerTime
|
||||||
|
),
|
||||||
// If RingRTC doesn't send us an aspect ratio, we make a guess.
|
// If RingRTC doesn't send us an aspect ratio, we make a guess.
|
||||||
videoAspectRatio:
|
videoAspectRatio:
|
||||||
remoteDeviceState.videoAspectRatio ||
|
remoteDeviceState.videoAspectRatio ||
|
||||||
|
|
|
@ -46,6 +46,7 @@ export interface GroupCallParticipantInfoType {
|
||||||
hasRemoteAudio: boolean;
|
hasRemoteAudio: boolean;
|
||||||
hasRemoteVideo: boolean;
|
hasRemoteVideo: boolean;
|
||||||
isSelf: boolean;
|
isSelf: boolean;
|
||||||
|
speakerTime?: number;
|
||||||
videoAspectRatio: number;
|
videoAspectRatio: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -102,6 +102,7 @@ const mapStateToActiveCallProp = (state: StateType) => {
|
||||||
isSelf: remoteParticipant.isSelf,
|
isSelf: remoteParticipant.isSelf,
|
||||||
name: remoteConversation.name,
|
name: remoteConversation.name,
|
||||||
profileName: remoteConversation.profileName,
|
profileName: remoteConversation.profileName,
|
||||||
|
speakerTime: remoteParticipant.speakerTime,
|
||||||
title: remoteConversation.title,
|
title: remoteConversation.title,
|
||||||
videoAspectRatio: remoteParticipant.videoAspectRatio,
|
videoAspectRatio: remoteParticipant.videoAspectRatio,
|
||||||
});
|
});
|
||||||
|
|
74
ts/test/util/ringrtc/normalizeGroupCallTimestamp_test.ts
Normal file
74
ts/test/util/ringrtc/normalizeGroupCallTimestamp_test.ts
Normal file
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -78,6 +78,7 @@ export interface GroupCallRemoteParticipantType {
|
||||||
isSelf: boolean;
|
isSelf: boolean;
|
||||||
name?: string;
|
name?: string;
|
||||||
profileName?: string;
|
profileName?: string;
|
||||||
|
speakerTime?: number;
|
||||||
title: string;
|
title: string;
|
||||||
videoAspectRatio: number;
|
videoAspectRatio: number;
|
||||||
}
|
}
|
||||||
|
|
36
ts/util/ringrtc/normalizeGroupCallTimestamp.ts
Normal file
36
ts/util/ringrtc/normalizeGroupCallTimestamp.ts
Normal file
|
@ -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;
|
||||||
|
}
|
|
@ -14293,9 +14293,9 @@ rimraf@~2.4.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
glob "^6.0.1"
|
glob "^6.0.1"
|
||||||
|
|
||||||
"ringrtc@https://github.com/signalapp/signal-ringrtc-node.git#e0c31ca67271850c736c10786139c65564330a73":
|
"ringrtc@https://github.com/signalapp/signal-ringrtc-node.git#eb01373e3279aab7ed3b718458af1cc5c45df63c":
|
||||||
version "2.8.3"
|
version "2.8.4"
|
||||||
resolved "https://github.com/signalapp/signal-ringrtc-node.git#e0c31ca67271850c736c10786139c65564330a73"
|
resolved "https://github.com/signalapp/signal-ringrtc-node.git#eb01373e3279aab7ed3b718458af1cc5c45df63c"
|
||||||
|
|
||||||
ripemd160@^2.0.0, ripemd160@^2.0.1:
|
ripemd160@^2.0.0, ripemd160@^2.0.1:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue