Merge branch 'livekit-experiment' into livekit-load-test

This commit is contained in:
Daniel Abramov 2023-06-14 17:12:03 +02:00
commit 520f241efa
13 changed files with 352 additions and 427 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>",
@ -102,7 +102,6 @@
"Turn off camera": "Turn off camera",
"Turn on camera": "Turn on camera",
"Unmute microphone": "Unmute microphone",
"Use the upcoming grid system": "Use the upcoming grid system",
"User menu": "User menu",
"Username": "Username",
"Version: {{version}}": "Version: {{version}}",

View file

@ -82,19 +82,6 @@ limitations under the License.
bottom: 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: calc(min(var(--tileWidth), var(--tileHeight)) / 2);
width: var(--avatarSize) !important;
height: var(--avatarSize) !important;
border-radius: 10000px !important;
}
@media (min-height: 300px) {
.inRoom {
--footerPadding: 24px;

View file

@ -28,7 +28,7 @@ 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";
import React, { useCallback, useEffect, useMemo, useRef } from "react";
import React, { Ref, useCallback, useEffect, useMemo, useRef } from "react";
import { useTranslation } from "react-i18next";
import useMeasure from "react-use-measure";
import { OverlayTriggerState } from "@react-stately/overlays";
@ -55,29 +55,29 @@ import {
useVideoGridLayout,
TileDescriptor,
} from "../video-grid/VideoGrid";
import { useNewGrid, useShowInspector } from "../settings/useSetting";
import { useShowInspector } from "../settings/useSetting";
import { useModalTriggerState } from "../Modal";
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
import { useUrlParams } from "../UrlParams";
import { MediaDevicesState } from "../settings/mediaDevices";
import { useRageshakeRequestModal } from "../settings/submit-rageshake";
import { useCallViewKeyboardShortcuts } from "../useCallViewKeyboardShortcuts";
import { usePrefersReducedMotion } from "../usePrefersReducedMotion";
import { ItemData, VideoTileContainer } from "../video-grid/VideoTileContainer";
import { ElementWidgetActions, widget } from "../widget";
import { GridLayoutMenu } from "./GridLayoutMenu";
import { GroupCallInspector } from "./GroupCallInspector";
import styles from "./InCallView.module.css";
import { RageshakeRequestModal } from "./RageshakeRequestModal";
import { MatrixInfo } from "./VideoPreview";
import { useJoinRule } from "./useJoinRule";
import { ParticipantInfo } from "./useGroupCall";
import { TileContent } from "../video-grid/VideoTile";
import { ItemData, TileContent } from "../video-grid/VideoTile";
import { Config } from "../config/Config";
import { NewVideoGrid } from "../video-grid/NewVideoGrid";
import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership";
import { SettingsModal } from "../settings/SettingsModal";
import { InviteModal } from "./InviteModal";
import { useRageshakeRequestModal } from "../settings/submit-rageshake";
import { RageshakeRequestModal } from "./RageshakeRequestModal";
import { VideoTile } from "../video-grid/VideoTile";
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
@ -258,8 +258,9 @@ export function InCallView({
[noControls, items]
);
const [newGrid] = useNewGrid();
const Grid = newGrid ? NewVideoGrid : VideoGrid;
const Grid =
items.length > 12 && layout === "freedom" ? NewVideoGrid : VideoGrid;
const prefersReducedMotion = usePrefersReducedMotion();
const renderContent = (): JSX.Element => {
@ -272,12 +273,11 @@ export function InCallView({
}
if (maximisedParticipant) {
return (
<VideoTileContainer
<VideoTile
targetHeight={bounds.height}
targetWidth={bounds.width}
id={maximisedParticipant.id}
key={maximisedParticipant.id}
item={maximisedParticipant.data}
data={maximisedParticipant.data}
/>
);
}
@ -288,7 +288,9 @@ export function InCallView({
layout={layout}
disableAnimations={prefersReducedMotion || isSafari}
>
{(child) => <VideoTileContainer item={child.data} {...child} />}
{(props) => (
<VideoTile {...props} ref={props.ref as Ref<HTMLDivElement>} />
)}
</Grid>
);
};
@ -462,7 +464,6 @@ function useParticipantTiles(
focused: false,
local: sfuParticipant.isLocal,
data: {
id,
member,
sfuParticipant,
content: TileContent.UserMedia,

View file

@ -33,7 +33,6 @@ import { MediaDevicesState } from "./mediaDevices";
import {
useShowInspector,
useOptInAnalytics,
useNewGrid,
useDeveloperSettingsTab,
} from "./useSetting";
import { FieldRow, InputField } from "../input/Input";
@ -60,7 +59,6 @@ export const SettingsModal = (props: Props) => {
const [optInAnalytics, setOptInAnalytics] = useOptInAnalytics();
const [developerSettingsTab, setDeveloperSettingsTab] =
useDeveloperSettingsTab();
const [newGrid, setNewGrid] = useNewGrid();
const downloadDebugLog = useDownloadDebugLog();
@ -235,17 +233,6 @@ export const SettingsModal = (props: Props) => {
}
/>
</FieldRow>
<FieldRow>
<InputField
id="newGrid"
label={t("Use the upcoming grid system")}
type="checkbox"
checked={newGrid}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setNewGrid(e.target.checked)
}
/>
</FieldRow>
<FieldRow>
<Button onPress={downloadDebugLog}>
{t("Download debug logs")}

View file

@ -98,7 +98,5 @@ export const useOptInAnalytics = (): DisableableSetting<boolean | null> => {
return [false, null];
};
export const useNewGrid = () => useSetting("new-grid", false);
export const useDeveloperSettingsTab = () =>
useSetting("developer-settings-tab", false);

View file

@ -21,14 +21,14 @@ import { MutableRefObject, RefCallback, useCallback } from "react";
* same DOM node.
*/
export const useMergedRefs = <T>(
...refs: (MutableRefObject<T | null> | RefCallback<T | null>)[]
...refs: (MutableRefObject<T | null> | RefCallback<T | null> | null)[]
): RefCallback<T | null> =>
useCallback(
(value) =>
refs.forEach((ref) => {
if (typeof ref === "function") {
ref(value);
} else {
} else if (ref !== null) {
ref.current = value;
}
}),

View file

@ -18,7 +18,6 @@ import { SpringRef, TransitionFn, useTransition } from "@react-spring/web";
import { EventTypes, Handler, useScroll } from "@use-gesture/react";
import React, {
Dispatch,
FC,
ReactNode,
SetStateAction,
useCallback,
@ -35,6 +34,7 @@ import {
VideoGridProps as Props,
TileSpring,
TileDescriptor,
ChildrenProperties,
} from "./VideoGrid";
import { useReactiveState } from "../useReactiveState";
import { useMergedRefs } from "../useMergedRefs";
@ -48,6 +48,7 @@ import {
cycleTileSize,
appendItems,
} from "./model";
import { TileWrapper } from "./TileWrapper";
interface GridState extends Grid {
/**
@ -144,11 +145,11 @@ interface DragState {
/**
* An interactive, animated grid of video tiles.
*/
export const NewVideoGrid: FC<Props<unknown>> = ({
export function NewVideoGrid<T>({
items,
disableAnimations,
children,
}) => {
}: Props<T>) {
// Overview: This component lays out tiles by rendering an invisible template
// grid of "slots" for tiles to go in. Once rendered, it uses the DOM API to
// get the dimensions of each slot, feeding these numbers back into
@ -455,16 +456,19 @@ export const NewVideoGrid: FC<Props<unknown>> = ({
>
{slots}
</div>
{tileTransitions((style, tile) =>
children({
...style,
key: tile.item.id,
targetWidth: tile.width,
targetHeight: tile.height,
data: tile.item.data,
onDragRef: onTileDragRef,
})
)}
{tileTransitions((spring, tile) => (
<TileWrapper
key={tile.item.id}
id={tile.item.id}
onDragRef={onTileDragRef}
targetWidth={tile.width}
targetHeight={tile.height}
data={tile.item.data}
{...spring}
>
{children as (props: ChildrenProperties<unknown>) => ReactNode}
</TileWrapper>
))}
</div>
);
};
}

View file

@ -0,0 +1,100 @@
/*
Copyright 2023 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 React, { FC, memo, ReactNode, RefObject, useRef } from "react";
import { EventTypes, Handler, useDrag } from "@use-gesture/react";
import { SpringValue, to } from "@react-spring/web";
import { ChildrenProperties } from "./VideoGrid";
interface Props<T> {
id: string;
onDragRef: RefObject<
(
tileId: string,
state: Parameters<Handler<"drag", EventTypes["drag"]>>[0]
) => void
>;
targetWidth: number;
targetHeight: number;
data: T;
opacity: SpringValue<number>;
scale: SpringValue<number>;
shadow: SpringValue<number>;
shadowSpread: SpringValue<number>;
zIndex: SpringValue<number>;
x: SpringValue<number>;
y: SpringValue<number>;
width: SpringValue<number>;
height: SpringValue<number>;
children: (props: ChildrenProperties<T>) => ReactNode;
}
/**
* A wrapper around a tile in a video grid. This component exists to decouple
* child components from the grid.
*/
export const TileWrapper: FC<Props<unknown>> = memo(
({
id,
onDragRef,
targetWidth,
targetHeight,
data,
opacity,
scale,
shadow,
shadowSpread,
zIndex,
x,
y,
width,
height,
children,
}) => {
const ref = useRef<HTMLElement | null>(null);
useDrag((state) => onDragRef?.current!(id, state), {
target: ref,
filterTaps: true,
preventScroll: true,
});
return (
<>
{children({
ref,
style: {
opacity,
scale,
zIndex,
x,
y,
width,
height,
boxShadow: to(
[shadow, shadowSpread],
(s, ss) => `rgba(0, 0, 0, 0.5) 0px ${s}px ${2 * s}px ${ss}px`
),
},
targetWidth,
targetHeight,
data,
})}
</>
);
}
);

View file

@ -1,61 +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 React, { useState } from "react";
import { useVideoGridLayout } from "./VideoGrid";
import { Button } from "../button";
export default {
title: "VideoGrid",
parameters: {
layout: "fullscreen",
},
};
const ParticipantsTest = () => {
const { layout, setLayout } = useVideoGridLayout(false);
const [participantCount, setParticipantCount] = useState(1);
return (
<>
<div style={{ display: "flex", width: "100vw", height: "32px" }}>
<Button
onPress={() =>
setLayout(layout === "freedom" ? "spotlight" : "freedom")
}
>
Toggle Layout
</Button>
{participantCount < 12 && (
<Button onPress={() => setParticipantCount((count) => count + 1)}>
Add Participant
</Button>
)}
{participantCount > 0 && (
<Button onPress={() => setParticipantCount((count) => count - 1)}>
Remove Participant
</Button>
)}
</div>
</>
);
};
ParticipantsTest.args = {
layout: "freedom",
participantCount: 1,
};

View file

@ -1,5 +1,5 @@
/*
Copyright 2022 New Vector Ltd
Copyright 2022-2023 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.
@ -15,8 +15,10 @@ limitations under the License.
*/
import React, {
ComponentProps,
Key,
RefObject,
ReactNode,
Ref,
useCallback,
useEffect,
useRef,
@ -26,21 +28,20 @@ import {
EventTypes,
FullGestureState,
Handler,
useDrag,
useGesture,
} from "@use-gesture/react";
import {
animated,
SpringRef,
SpringValue,
SpringValues,
useSprings,
} from "@react-spring/web";
import useMeasure from "react-use-measure";
import { ResizeObserver as JuggleResizeObserver } from "@juggle/resize-observer";
import { ReactDOMAttributes } from "@use-gesture/react/dist/declarations/src/types";
import styles from "./VideoGrid.module.css";
import { Layout } from "../room/GridLayoutMenu";
import { TileWrapper } from "./TileWrapper";
interface TilePosition {
x: number;
@ -51,7 +52,7 @@ interface TilePosition {
}
interface Tile<T> {
key: Key;
key: string;
order: number;
item: TileDescriptor<T>;
remove: boolean;
@ -727,25 +728,18 @@ interface DragTileData {
y: number;
}
export interface ChildrenProperties<T> extends ReactDOMAttributes {
key: Key;
data: T;
export interface ChildrenProperties<T> {
ref: Ref<HTMLElement>;
style: ComponentProps<typeof animated.div>["style"];
/**
* The width this tile will have once its animations have settled.
*/
targetWidth: number;
/**
* The height this tile will have once its animations have settled.
*/
targetHeight: number;
opacity: SpringValue<number>;
scale: SpringValue<number>;
shadow: SpringValue<number>;
zIndex: SpringValue<number>;
x: SpringValue<number>;
y: SpringValue<number>;
width: SpringValue<number>;
height: SpringValue<number>;
onDragRef?: RefObject<
(
tileId: string,
state: Parameters<Handler<"drag", EventTypes["drag"]>>[0]
) => void
>;
data: T;
}
export interface VideoGridProps<T> {
@ -768,7 +762,7 @@ export function VideoGrid<T>({
items,
layout,
disableAnimations,
children: createChild,
children,
}: VideoGridProps<T>) {
// Place the PiP in the bottom right corner by default
const [pipXRatio, setPipXRatio] = useState(1);
@ -1082,117 +1076,132 @@ export function VideoGrid<T>({
[tiles, layout, gridBounds.width, gridBounds.height, pipXRatio, pipYRatio]
);
const bindTile = useDrag(
({ args: [key], active, xy, movement, tap, last, event }) => {
event.preventDefault();
// Callback for useDrag. We could call useDrag here, but the default
// pattern of spreading {...bind()} across the children to bind the gesture
// ends up breaking memoization and ruining this component's performance.
// Instead, we pass this callback to each tile via a ref, to let them bind the
// gesture using the much more sensible ref-based method.
const onTileDrag = (
tileId: string,
{
active,
xy,
movement,
tap,
last,
event,
}: Parameters<Handler<"drag", EventTypes["drag"]>>[0]
) => {
event.preventDefault();
if (tap) {
onTap(key);
return;
}
if (tap) {
onTap(tileId);
return;
}
if (layout !== "freedom") return;
if (layout !== "freedom") return;
const dragTileIndex = tiles.findIndex((tile) => tile.key === key);
const dragTile = tiles[dragTileIndex];
const dragTilePosition = tilePositions[dragTile.order];
const dragTileIndex = tiles.findIndex((tile) => tile.key === tileId);
const dragTile = tiles[dragTileIndex];
const dragTilePosition = tilePositions[dragTile.order];
const cursorPosition = [xy[0] - gridBounds.left, xy[1] - gridBounds.top];
const cursorPosition = [xy[0] - gridBounds.left, xy[1] - gridBounds.top];
let newTiles = tiles;
let newTiles = tiles;
if (tiles.length === 2 && !tiles.some((t) => t.focused)) {
// We're in 1:1 mode, so only the local tile should be draggable
if (!dragTile.item.local) return;
if (tiles.length === 2 && !tiles.some((t) => t.focused)) {
// We're in 1:1 mode, so only the local tile should be draggable
if (!dragTile.item.local) return;
// Position should only update on the very last event, to avoid
// compounding the offset on every drag event
if (last) {
const remotePosition = tilePositions[1];
// Position should only update on the very last event, to avoid
// compounding the offset on every drag event
if (last) {
const remotePosition = tilePositions[1];
const pipGap = getPipGap(
gridBounds.width / gridBounds.height,
gridBounds.width
);
const pipMinX = remotePosition.x + pipGap;
const pipMinY = remotePosition.y + pipGap;
const pipMaxX =
remotePosition.x +
remotePosition.width -
dragTilePosition.width -
pipGap;
const pipMaxY =
remotePosition.y +
remotePosition.height -
dragTilePosition.height -
pipGap;
const newPipXRatio =
(dragTilePosition.x + movement[0] - pipMinX) / (pipMaxX - pipMinX);
const newPipYRatio =
(dragTilePosition.y + movement[1] - pipMinY) / (pipMaxY - pipMinY);
setPipXRatio(Math.max(0, Math.min(1, newPipXRatio)));
setPipYRatio(Math.max(0, Math.min(1, newPipYRatio)));
}
} else {
const hoverTile = tiles.find(
(tile) =>
tile.key !== key &&
isInside(cursorPosition, tilePositions[tile.order])
const pipGap = getPipGap(
gridBounds.width / gridBounds.height,
gridBounds.width
);
const pipMinX = remotePosition.x + pipGap;
const pipMinY = remotePosition.y + pipGap;
const pipMaxX =
remotePosition.x +
remotePosition.width -
dragTilePosition.width -
pipGap;
const pipMaxY =
remotePosition.y +
remotePosition.height -
dragTilePosition.height -
pipGap;
if (hoverTile) {
// Shift the tiles into their new order
newTiles = newTiles.map((tile) => {
let order = tile.order;
if (order < dragTile.order) {
if (order >= hoverTile.order) order++;
} else if (order > dragTile.order) {
if (order <= hoverTile.order) order--;
} else {
order = hoverTile.order;
}
const newPipXRatio =
(dragTilePosition.x + movement[0] - pipMinX) / (pipMaxX - pipMinX);
const newPipYRatio =
(dragTilePosition.y + movement[1] - pipMinY) / (pipMaxY - pipMinY);
let focused;
if (tile === hoverTile) {
focused = dragTile.focused;
} else if (tile === dragTile) {
focused = hoverTile.focused;
} else {
focused = tile.focused;
}
return { ...tile, order, focused };
});
reorderTiles(newTiles, layout);
setTileState((state) => ({ ...state, tiles: newTiles }));
}
setPipXRatio(Math.max(0, Math.min(1, newPipXRatio)));
setPipYRatio(Math.max(0, Math.min(1, newPipYRatio)));
}
} else {
const hoverTile = tiles.find(
(tile) =>
tile.key !== tileId &&
isInside(cursorPosition, tilePositions[tile.order])
);
if (active) {
if (!draggingTileRef.current) {
draggingTileRef.current = {
key: dragTile.key,
offsetX: dragTilePosition.x,
offsetY: dragTilePosition.y,
x: movement[0],
y: movement[1],
};
} else {
draggingTileRef.current.x = movement[0];
draggingTileRef.current.y = movement[1];
}
if (hoverTile) {
// Shift the tiles into their new order
newTiles = newTiles.map((tile) => {
let order = tile.order;
if (order < dragTile.order) {
if (order >= hoverTile.order) order++;
} else if (order > dragTile.order) {
if (order <= hoverTile.order) order--;
} else {
order = hoverTile.order;
}
let focused;
if (tile === hoverTile) {
focused = dragTile.focused;
} else if (tile === dragTile) {
focused = hoverTile.focused;
} else {
focused = tile.focused;
}
return { ...tile, order, focused };
});
reorderTiles(newTiles, layout);
setTileState((state) => ({ ...state, tiles: newTiles }));
}
}
if (active) {
if (!draggingTileRef.current) {
draggingTileRef.current = {
key: dragTile.key,
offsetX: dragTilePosition.x,
offsetY: dragTilePosition.y,
x: movement[0],
y: movement[1],
};
} else {
draggingTileRef.current = null;
draggingTileRef.current.x = movement[0];
draggingTileRef.current.y = movement[1];
}
} else {
draggingTileRef.current = null;
}
api.start(animate(newTiles));
},
{ filterTaps: true, pointer: { buttons: [1] } }
);
api.start(animate(newTiles));
};
const onTileDragRef = useRef(onTileDrag);
onTileDragRef.current = onTileDrag;
const onGridGesture = useCallback(
(
@ -1239,18 +1248,23 @@ export function VideoGrid<T>({
return (
<div className={styles.videoGrid} ref={gridRef} {...bindGrid()}>
{springs.map((style, i) => {
{springs.map((spring, i) => {
const tile = tiles[i];
const tilePosition = tilePositions[tile.order];
return createChild({
...bindTile(tile.key),
...style,
key: tile.key,
data: tile.item.data,
targetWidth: tilePosition.width,
targetHeight: tilePosition.height,
});
return (
<TileWrapper
key={tile.key}
id={tile.key}
onDragRef={onTileDragRef}
targetWidth={tilePosition.width}
targetHeight={tilePosition.height}
data={tile.item.data}
{...spring}
>
{children as (props: ChildrenProperties<unknown>) => ReactNode}
</TileWrapper>
);
})}
</div>
);

View file

@ -18,12 +18,10 @@ limitations under the License.
position: absolute;
contain: strict;
top: 0;
width: var(--tileWidth);
height: var(--tileHeight);
container-name: videoTile;
container-type: size;
--tileRadius: 8px;
border-radius: var(--tileRadius);
box-shadow: rgba(0, 0, 0, 0.5) 0px var(--tileShadow)
calc(2 * var(--tileShadow)) var(--tileShadowSpread);
overflow: hidden;
cursor: pointer;
@ -195,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

@ -1,5 +1,5 @@
/*
Copyright 2022 New Vector Ltd
Copyright 2022-2023 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.
@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { ForwardedRef, forwardRef } from "react";
import { animated, SpringValue } from "@react-spring/web";
import React from "react";
import { animated } from "@react-spring/web";
import classNames from "classnames";
import { useTranslation } from "react-i18next";
import { LocalParticipant, RemoteParticipant, Track } from "livekit-client";
@ -24,59 +24,60 @@ import {
VideoTrack,
useMediaTrack,
} from "@livekit/components-react";
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";
export interface ItemData {
member?: RoomMember;
sfuParticipant: LocalParticipant | RemoteParticipant;
content: TileContent;
}
export enum TileContent {
UserMedia = "user-media",
ScreenShare = "screen-share",
}
interface Props {
name: string;
sfuParticipant: LocalParticipant | RemoteParticipant;
content: TileContent;
data: ItemData;
// TODO: Refactor this set of props.
// See https://github.com/vector-im/element-call/pull/1099#discussion_r1226863404
avatar?: JSX.Element;
// TODO: Refactor these props.
targetWidth: number;
targetHeight: number;
className?: string;
opacity?: SpringValue<number>;
scale?: SpringValue<number>;
shadow?: SpringValue<number>;
shadowSpread?: SpringValue<number>;
zIndex?: SpringValue<number>;
x?: SpringValue<number>;
y?: SpringValue<number>;
width?: SpringValue<number>;
height?: SpringValue<number>;
style?: React.ComponentProps<typeof animated.div>["style"];
}
export const VideoTile = forwardRef<HTMLElement, Props>(
(
{
name,
sfuParticipant,
content,
avatar,
className,
opacity,
scale,
shadow,
shadowSpread,
zIndex,
x,
y,
width,
height,
...rest
},
ref
) => {
export const VideoTile = React.forwardRef<HTMLDivElement, Props>(
({ data, className, style, targetWidth, targetHeight }, tileRef) => {
const { t } = useTranslation();
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(
content === TileContent.UserMedia
@ -88,6 +89,9 @@ export const VideoTile = forwardRef<HTMLElement, Props>(
}
);
// Firefox doesn't respect the disablePictureInPicture attribute
// https://bugzilla.mozilla.org/show_bug.cgi?id=1611831
return (
<animated.div
className={classNames(styles.videoTile, className, {
@ -96,38 +100,30 @@ export const VideoTile = forwardRef<HTMLElement, Props>(
[styles.muted]: microphoneMuted,
[styles.screenshare]: content === TileContent.ScreenShare,
})}
style={{
opacity,
scale,
zIndex,
x,
y,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore React does in fact support assigning custom properties,
// but React's types say no
"--tileWidth": width?.to((w) => `${w}px`),
"--tileHeight": height?.to((h) => `${h}px`),
"--tileShadow": shadow?.to((s) => `${s}px`),
"--tileShadowSpread": shadowSpread?.to((s) => `${s}px`),
}}
ref={ref as ForwardedRef<HTMLDivElement>}
style={style}
ref={tileRef}
data-testid="videoTile"
{...rest}
>
{!sfuParticipant.isCameraEnabled && (
{content === TileContent.UserMedia && !sfuParticipant.isCameraEnabled && (
<>
<div className={styles.videoMutedOverlay} />
{avatar}
<Avatar
key={member?.userId}
size={Math.round(Math.min(targetWidth, targetHeight) / 2)}
src={member?.getMxcAvatarUrl()}
fallback={displayName.slice(0, 1).toUpperCase()}
className={styles.avatar}
/>
</>
)}
{sfuParticipant.isScreenShareEnabled ? (
{content == TileContent.ScreenShare ? (
<div className={styles.presenterLabel}>
<span>{t("{{name}} is presenting", { name })}</span>
<span>{t("{{displayName}} is presenting", { displayName })}</span>
</div>
) : (
<div className={classNames(styles.infoBubble, styles.memberName)}>
{microphoneMuted ? <MicMutedIcon /> : <MicIcon />}
<span title={name}>{name}</span>
<span title={displayName}>{displayName}</span>
<ConnectionQualityIndicator participant={sfuParticipant} />
</div>
)}

View file

@ -1,115 +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 React from "react";
import {
RoomMember,
RoomMemberEvent,
} from "matrix-js-sdk/src/models/room-member";
import { LocalParticipant, RemoteParticipant } from "livekit-client";
import { SpringValue } from "@react-spring/web";
import { EventTypes, Handler, useDrag } from "@use-gesture/react";
import { TileContent, VideoTile } from "./VideoTile";
import { Avatar } from "../Avatar";
import Styles from "../room/InCallView.module.css";
export interface ItemData {
member?: RoomMember;
sfuParticipant: LocalParticipant | RemoteParticipant;
content: TileContent;
}
interface Props {
item: ItemData;
// TODO: Refactor this set of props.
// See https://github.com/vector-im/element-call/pull/1099#discussion_r1226863404
id: string;
targetWidth: number;
targetHeight: number;
opacity?: SpringValue<number>;
scale?: SpringValue<number>;
shadow?: SpringValue<number>;
shadowSpread?: SpringValue<number>;
zIndex?: SpringValue<number>;
x?: SpringValue<number>;
y?: SpringValue<number>;
width?: SpringValue<number>;
height?: SpringValue<number>;
onDragRef?: React.RefObject<
(
tileId: string,
state: Parameters<Handler<"drag", EventTypes["drag"]>>[0]
) => void
>;
}
export const VideoTileContainer: React.FC<Props> = React.memo(
({ item, id, targetWidth, targetHeight, onDragRef, ...rest }) => {
// Handle display name changes.
const [displayName, setDisplayName] = React.useState<string>("[👻]");
React.useEffect(() => {
const member = item.member;
if (member) {
setDisplayName(member.rawDisplayName);
const updateName = () => {
setDisplayName(member.rawDisplayName);
};
member!.on(RoomMemberEvent.Name, updateName);
return () => {
member!.removeListener(RoomMemberEvent.Name, updateName);
};
}
}, [item.member]);
// Create an avatar.
const avatar = (
<Avatar
key={item.member?.userId}
size={Math.round(Math.min(targetWidth, targetHeight) / 2)}
src={item.member?.getMxcAvatarUrl()}
fallback={displayName.slice(0, 1).toUpperCase()}
className={Styles.avatar}
/>
);
// Make sure that the tile is draggable and work well within video grid layout.
//
// Firefox doesn't respect the disablePictureInPicture attribute
// https://bugzilla.mozilla.org/show_bug.cgi?id=1611831
const tileRef = React.useRef<HTMLElement | null>(null);
useDrag((state) => onDragRef?.current!(id, state), {
target: tileRef,
filterTaps: true,
preventScroll: true,
});
return (
<VideoTile
ref={tileRef}
sfuParticipant={item.sfuParticipant}
content={item.content}
name={displayName}
avatar={avatar}
{...rest}
/>
);
}
);