Merge pull request #1106 from vector-im/livekit-load-test

Display SFU participants who did not publish an `m.call.member` event
This commit is contained in:
Daniel Abramov 2023-06-14 22:17:08 +02:00 committed by GitHub
commit 29e748a8e1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 95 additions and 167 deletions

View file

@ -1,8 +1,8 @@
{
"{{count}} stars|one": "{{count}} star",
"{{count}} stars|other": "{{count}} stars",
"{{displayName}} is presenting": "{{displayName}} is presenting",
"{{displayName}}, your call has ended.": "{{displayName}}, your call has ended.",
"{{name}} is presenting": "{{name}} is presenting",
"{{names}}, {{name}}": "{{names}}, {{name}}",
"<0></0><1></1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "<0></0><1></1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.",
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>",

View file

@ -82,23 +82,6 @@ limitations under the License.
bottom: 0;
}
/* CSS makes us put a condition here, even though all we want to do is
unconditionally select the container so we can use cqmin units */
@container videoTile (width > 0) {
.avatar {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
/* To make avatars scale smoothly with their tiles during animations, we
override the styles set on the element */
--avatarSize: 50cqmin; /* Half of the smallest dimension of the tile */
width: var(--avatarSize) !important;
height: var(--avatarSize) !important;
border-radius: 10000px !important;
}
}
@media (min-height: 300px) {
.inRoom {
--footerPadding: 24px;

View file

@ -23,12 +23,7 @@ import {
} from "@livekit/components-react";
import { usePreventScroll } from "@react-aria/overlays";
import classNames from "classnames";
import {
LocalParticipant,
RemoteParticipant,
Room,
Track,
} from "livekit-client";
import { Room, Track } from "livekit-client";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
@ -59,7 +54,6 @@ import {
useVideoGridLayout,
TileDescriptor,
} from "../video-grid/VideoGrid";
import { Avatar } from "../Avatar";
import { useShowInspector } from "../settings/useSetting";
import { useModalTriggerState } from "../Modal";
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
@ -275,26 +269,9 @@ export function InCallView({
[noControls, items]
);
const renderAvatar = useCallback(
(roomMember: RoomMember, width: number, height: number) => {
const avatarUrl = roomMember.getMxcAvatarUrl();
const size = Math.round(Math.min(width, height) / 2);
return (
<Avatar
key={roomMember.userId}
size={size}
src={avatarUrl ?? undefined}
fallback={roomMember.name.slice(0, 1).toUpperCase()}
className={styles.avatar}
/>
);
},
[]
);
const Grid =
items.length > 12 && layout === "freedom" ? NewVideoGrid : VideoGrid;
const prefersReducedMotion = usePrefersReducedMotion();
const renderContent = (): JSX.Element => {
@ -312,8 +289,6 @@ export function InCallView({
targetWidth={bounds.width}
key={maximisedParticipant.id}
data={maximisedParticipant.data}
getAvatar={renderAvatar}
showSpeakingIndicator={false}
/>
);
}
@ -325,12 +300,7 @@ export function InCallView({
disableAnimations={prefersReducedMotion || isSafari}
>
{(props) => (
<VideoTile
getAvatar={renderAvatar}
showSpeakingIndicator={items.length > 2}
{...props}
ref={props.ref as Ref<HTMLDivElement>}
/>
<VideoTile {...props} ref={props.ref as Ref<HTMLDivElement>} />
)}
</Grid>
);
@ -484,33 +454,29 @@ function useParticipantTiles(
});
const items = useMemo(() => {
const tiles: TileDescriptor<ItemData>[] = [];
const screenshareExists = sfuParticipants.some(
(p) => p.isScreenShareEnabled
// The IDs of the participants who published membership event to the room (i.e. are present from Matrix perspective).
const matrixParticipants: Map<string, RoomMember> = new Map(
[...participants.entries()].flatMap(([user, devicesMap]) => {
return [...devicesMap.keys()].map((deviceId) => [
`${user.userId}:${deviceId}`,
user,
]);
})
);
const participantsById = new Map<
string,
LocalParticipant | RemoteParticipant
>();
for (const p of sfuParticipants) participantsById.set(p.identity, p);
for (const [member, participantMap] of participants) {
for (const [deviceId] of participantMap) {
const id = `${member.userId}:${deviceId}`;
const sfuParticipant = participantsById.get(id);
const someoneIsPresenting = sfuParticipants.some((p) => {
!p.isLocal && p.isScreenShareEnabled;
});
// Skip rendering participants that did not connect to the SFU.
if (!sfuParticipant) {
continue;
}
// Iterate over SFU participants (those who actually are present from the SFU perspective) and create tiles for them.
const tiles: TileDescriptor<ItemData>[] = sfuParticipants.flatMap(
(sfuParticipant) => {
const id = sfuParticipant.identity;
const member = matrixParticipants.get(id);
const userMediaTile = {
id,
// Screenshare feeds take precedence for focus
focused:
!screenshareExists &&
sfuParticipant.isSpeaking &&
!sfuParticipant.isLocal,
focused: !someoneIsPresenting && sfuParticipant.isSpeaking,
local: sfuParticipant.isLocal,
data: {
member,
@ -519,12 +485,10 @@ function useParticipantTiles(
},
};
// Add a tile for user media.
tiles.push(userMediaTile);
// If there is a screen sharing enabled for this participant, create a tile for it as well.
let screenShareTile: TileDescriptor<ItemData> | undefined;
if (sfuParticipant.isScreenShareEnabled) {
const screenShareTile = {
screenShareTile = {
...userMediaTile,
id: `${id}:screen-share`,
focused: true,
@ -533,10 +497,13 @@ function useParticipantTiles(
content: TileContent.ScreenShare,
},
};
tiles.push(screenShareTile);
}
return screenShareTile
? [userMediaTile, screenShareTile]
: [userMediaTile];
}
}
);
PosthogAnalytics.instance.eventCallEnded.cacheParticipantCountChanged(
tiles.length

View file

@ -193,3 +193,20 @@ limitations under the License.
--tileRadius: 20px;
}
}
/* CSS makes us put a condition here, even though all we want to do is
unconditionally select the container so we can use cqmin units */
@container videoTile (width > 0) {
.avatar {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
/* To make avatars scale smoothly with their tiles during animations, we
override the styles set on the element */
--avatarSize: 50cqmin; /* Half of the smallest dimension of the tile */
width: var(--avatarSize) !important;
height: var(--avatarSize) !important;
border-radius: 10000px !important;
}
}

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { ComponentProps, forwardRef } from "react";
import React from "react";
import { animated } from "@react-spring/web";
import classNames from "classnames";
import { useTranslation } from "react-i18next";
@ -24,15 +24,18 @@ import {
VideoTrack,
useMediaTrack,
} from "@livekit/components-react";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import {
RoomMember,
RoomMemberEvent,
} from "matrix-js-sdk/src/models/room-member";
import { Avatar } from "../Avatar";
import styles from "./VideoTile.module.css";
import { ReactComponent as MicIcon } from "../icons/Mic.svg";
import { ReactComponent as MicMutedIcon } from "../icons/MicMuted.svg";
import { useRoomMemberName } from "./useRoomMemberName";
export interface ItemData {
member: RoomMember;
member?: RoomMember;
sfuParticipant: LocalParticipant | RemoteParticipant;
content: TileContent;
}
@ -44,35 +47,36 @@ export enum TileContent {
interface Props {
data: ItemData;
className?: string;
showSpeakingIndicator: boolean;
style?: ComponentProps<typeof animated.div>["style"];
// TODO: Refactor these props.
targetWidth: number;
targetHeight: number;
getAvatar: (
roomMember: RoomMember,
width: number,
height: number
) => JSX.Element;
className?: string;
style?: React.ComponentProps<typeof animated.div>["style"];
}
export const VideoTile = forwardRef<HTMLDivElement, Props>(
(
{
data,
className,
showSpeakingIndicator,
style,
targetWidth,
targetHeight,
getAvatar,
},
tileRef
) => {
export const VideoTile = React.forwardRef<HTMLDivElement, Props>(
({ data, className, style, targetWidth, targetHeight }, tileRef) => {
const { t } = useTranslation();
const { content, sfuParticipant } = data;
const { rawDisplayName: name } = useRoomMemberName(data.member);
const { content, sfuParticipant, member } = data;
// Handle display name changes.
const [displayName, setDisplayName] = React.useState<string>("[👻]");
React.useEffect(() => {
if (member) {
setDisplayName(member.rawDisplayName);
const updateName = () => {
setDisplayName(member.rawDisplayName);
};
member!.on(RoomMemberEvent.Name, updateName);
return () => {
member!.removeListener(RoomMemberEvent.Name, updateName);
};
}
}, [member]);
const audioEl = React.useRef<HTMLAudioElement>(null);
const { isMuted: microphoneMuted } = useMediaTrack(
@ -92,7 +96,7 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
<animated.div
className={classNames(styles.videoTile, className, {
[styles.isLocal]: sfuParticipant.isLocal,
[styles.speaking]: sfuParticipant.isSpeaking && showSpeakingIndicator,
[styles.speaking]: sfuParticipant.isSpeaking,
[styles.muted]: microphoneMuted,
[styles.screenshare]: content === TileContent.ScreenShare,
})}
@ -103,21 +107,26 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
{content === TileContent.UserMedia && !sfuParticipant.isCameraEnabled && (
<>
<div className={styles.videoMutedOverlay} />
{getAvatar(data.member, targetWidth, targetHeight)}
<Avatar
key={member?.userId}
size={Math.round(Math.min(targetWidth, targetHeight) / 2)}
src={member?.getMxcAvatarUrl()}
fallback={displayName.slice(0, 1).toUpperCase()}
className={styles.avatar}
/>
</>
)}
{!false &&
(content === TileContent.ScreenShare ? (
<div className={styles.presenterLabel}>
<span>{t("{{name}} is presenting", { name })}</span>
</div>
) : (
<div className={classNames(styles.infoBubble, styles.memberName)}>
{microphoneMuted ? <MicMutedIcon /> : <MicIcon />}
<span title={name}>{name}</span>
<ConnectionQualityIndicator participant={sfuParticipant} />
</div>
))}
{content == TileContent.ScreenShare ? (
<div className={styles.presenterLabel}>
<span>{t("{{displayName}} is presenting", { displayName })}</span>
</div>
) : (
<div className={classNames(styles.infoBubble, styles.memberName)}>
{microphoneMuted ? <MicMutedIcon /> : <MicIcon />}
<span title={displayName}>{displayName}</span>
<ConnectionQualityIndicator participant={sfuParticipant} />
</div>
)}
<VideoTrack
participant={sfuParticipant}
source={

View file

@ -1,48 +0,0 @@
/*
Copyright 2022 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import {
RoomMember,
RoomMemberEvent,
} from "matrix-js-sdk/src/models/room-member";
import { useState, useEffect } from "react";
interface RoomMemberName {
name: string;
rawDisplayName: string;
}
export function useRoomMemberName(member: RoomMember): RoomMemberName {
const [state, setState] = useState<RoomMemberName>({
name: member.name,
rawDisplayName: member.rawDisplayName,
});
useEffect(() => {
function updateName() {
setState({ name: member.name, rawDisplayName: member.rawDisplayName });
}
updateName();
member.on(RoomMemberEvent.Name, updateName);
return () => {
member.removeListener(RoomMemberEvent.Name, updateName);
};
}, [member]);
return state;
}