Merge remote-tracking branch 'upstream/main' into SimonBrandner/fix/audio
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
This commit is contained in:
		
				commit
				
					
						e82ed2cbcb
					
				
			
		
					 11 changed files with 338 additions and 171 deletions
				
			
		| 
						 | 
				
			
			@ -26,7 +26,7 @@ import menuStyles from "../Menu.module.css";
 | 
			
		|||
import { Menu } from "../Menu";
 | 
			
		||||
import { TooltipTrigger } from "../Tooltip";
 | 
			
		||||
 | 
			
		||||
type Layout = "freedom" | "spotlight";
 | 
			
		||||
export type Layout = "freedom" | "spotlight";
 | 
			
		||||
interface Props {
 | 
			
		||||
  layout: Layout;
 | 
			
		||||
  setLayout: (layout: Layout) => void;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,7 +16,7 @@ limitations under the License.
 | 
			
		|||
 | 
			
		||||
import React, { useCallback, useMemo, useRef } from "react";
 | 
			
		||||
import { usePreventScroll } from "@react-aria/overlays";
 | 
			
		||||
import { GroupCall, MatrixClient } from "matrix-js-sdk";
 | 
			
		||||
import { GroupCall, MatrixClient, RoomMember } from "matrix-js-sdk";
 | 
			
		||||
import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
 | 
			
		||||
import classNames from "classnames";
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -79,10 +79,10 @@ interface Props {
 | 
			
		|||
 | 
			
		||||
export interface Participant {
 | 
			
		||||
  id: string;
 | 
			
		||||
  callFeed: CallFeed;
 | 
			
		||||
  focused: boolean;
 | 
			
		||||
  isLocal: boolean;
 | 
			
		||||
  presenter: boolean;
 | 
			
		||||
  callFeed?: CallFeed;
 | 
			
		||||
  isLocal?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function InCallView({
 | 
			
		||||
| 
						 | 
				
			
			@ -106,7 +106,7 @@ export function InCallView({
 | 
			
		|||
}: Props) {
 | 
			
		||||
  usePreventScroll();
 | 
			
		||||
  const elementRef = useRef<HTMLDivElement>();
 | 
			
		||||
  const [layout, setLayout] = useVideoGridLayout(screenshareFeeds.length > 0);
 | 
			
		||||
  const { layout, setLayout } = useVideoGridLayout(screenshareFeeds.length > 0);
 | 
			
		||||
  const { toggleFullscreen, fullscreenParticipant } = useFullscreen(elementRef);
 | 
			
		||||
 | 
			
		||||
  const [spatialAudio] = useSpatialAudio();
 | 
			
		||||
| 
						 | 
				
			
			@ -157,20 +157,23 @@ export function InCallView({
 | 
			
		|||
    return participants;
 | 
			
		||||
  }, [userMediaFeeds, activeSpeaker, screenshareFeeds, layout]);
 | 
			
		||||
 | 
			
		||||
  const renderAvatar = useCallback((roomMember, width, height) => {
 | 
			
		||||
    const avatarUrl = roomMember.user?.avatarUrl;
 | 
			
		||||
    const size = Math.round(Math.min(width, height) / 2);
 | 
			
		||||
  const renderAvatar = useCallback(
 | 
			
		||||
    (roomMember: RoomMember, width: number, height: number) => {
 | 
			
		||||
      const avatarUrl = roomMember.user?.avatarUrl;
 | 
			
		||||
      const size = Math.round(Math.min(width, height) / 2);
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <Avatar
 | 
			
		||||
        key={roomMember.userId}
 | 
			
		||||
        size={size}
 | 
			
		||||
        src={avatarUrl}
 | 
			
		||||
        fallback={roomMember.name.slice(0, 1).toUpperCase()}
 | 
			
		||||
        className={styles.avatar}
 | 
			
		||||
      />
 | 
			
		||||
    );
 | 
			
		||||
  }, []);
 | 
			
		||||
      return (
 | 
			
		||||
        <Avatar
 | 
			
		||||
          key={roomMember.userId}
 | 
			
		||||
          size={size}
 | 
			
		||||
          src={avatarUrl}
 | 
			
		||||
          fallback={roomMember.name.slice(0, 1).toUpperCase()}
 | 
			
		||||
          className={styles.avatar}
 | 
			
		||||
        />
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
    []
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const renderContent = useCallback((): JSX.Element => {
 | 
			
		||||
    if (items.length === 0) {
 | 
			
		||||
| 
						 | 
				
			
			@ -189,7 +192,7 @@ export function InCallView({
 | 
			
		|||
          audioContext={audioContext}
 | 
			
		||||
          audioDestination={audioDestination}
 | 
			
		||||
          disableSpeakingIndicator={true}
 | 
			
		||||
          isFullscreen={fullscreenParticipant}
 | 
			
		||||
          isFullscreen={!!fullscreenParticipant}
 | 
			
		||||
          onFullscreen={toggleFullscreen}
 | 
			
		||||
        />
 | 
			
		||||
      );
 | 
			
		||||
| 
						 | 
				
			
			@ -202,11 +205,11 @@ export function InCallView({
 | 
			
		|||
            key={item.id}
 | 
			
		||||
            item={item}
 | 
			
		||||
            getAvatar={renderAvatar}
 | 
			
		||||
            showName={items.length > 2 || item.focused}
 | 
			
		||||
            audioOutputDevice={audioOutput}
 | 
			
		||||
            audioContext={audioContext}
 | 
			
		||||
            audioDestination={audioDestination}
 | 
			
		||||
            disableSpeakingIndicator={items.length < 3}
 | 
			
		||||
            isFullscreen={fullscreenParticipant}
 | 
			
		||||
            isFullscreen={!!fullscreenParticipant}
 | 
			
		||||
            onFullscreen={toggleFullscreen}
 | 
			
		||||
            {...rest}
 | 
			
		||||
          />
 | 
			
		||||
| 
						 | 
				
			
			@ -221,6 +224,7 @@ export function InCallView({
 | 
			
		|||
    layout,
 | 
			
		||||
    renderAvatar,
 | 
			
		||||
    toggleFullscreen,
 | 
			
		||||
    audioOutput,
 | 
			
		||||
  ]);
 | 
			
		||||
 | 
			
		||||
  const {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -39,7 +39,7 @@ export function AudioForParticipant({
 | 
			
		|||
  const sourceRef = useRef<MediaStreamAudioSourceNode>();
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (!item.isLocal && stream.getAudioTracks().length > 0 && audioContext) {
 | 
			
		||||
    if (!item.isLocal && audioContext) {
 | 
			
		||||
      if (!gainNodeRef.current) {
 | 
			
		||||
        gainNodeRef.current = new GainNode(audioContext, {
 | 
			
		||||
          gain: localVolume,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,10 +15,12 @@ limitations under the License.
 | 
			
		|||
*/
 | 
			
		||||
 | 
			
		||||
import React, { useState } from "react";
 | 
			
		||||
import { useMemo } from "react";
 | 
			
		||||
 | 
			
		||||
import { VideoGrid, useVideoGridLayout } from "./VideoGrid";
 | 
			
		||||
import { VideoTile } from "./VideoTile";
 | 
			
		||||
import { useMemo } from "react";
 | 
			
		||||
import { Button } from "../button";
 | 
			
		||||
import { Participant } from "../room/InCallView";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  title: "VideoGrid",
 | 
			
		||||
| 
						 | 
				
			
			@ -28,10 +30,10 @@ export default {
 | 
			
		|||
};
 | 
			
		||||
 | 
			
		||||
export const ParticipantsTest = () => {
 | 
			
		||||
  const [layout, setLayout] = useVideoGridLayout(false);
 | 
			
		||||
  const { layout, setLayout } = useVideoGridLayout(false);
 | 
			
		||||
  const [participantCount, setParticipantCount] = useState(1);
 | 
			
		||||
 | 
			
		||||
  const items = useMemo(
 | 
			
		||||
  const items: Participant[] = useMemo(
 | 
			
		||||
    () =>
 | 
			
		||||
      new Array(participantCount).fill(undefined).map((_, i) => ({
 | 
			
		||||
        id: (i + 1).toString(),
 | 
			
		||||
| 
						 | 
				
			
			@ -46,9 +48,7 @@ export const ParticipantsTest = () => {
 | 
			
		|||
      <div style={{ display: "flex", width: "100vw", height: "32px" }}>
 | 
			
		||||
        <Button
 | 
			
		||||
          onPress={() =>
 | 
			
		||||
            setLayout((layout) =>
 | 
			
		||||
              layout === "freedom" ? "spotlight" : "freedom"
 | 
			
		||||
            )
 | 
			
		||||
            setLayout(layout === "freedom" ? "spotlight" : "freedom")
 | 
			
		||||
          }
 | 
			
		||||
        >
 | 
			
		||||
          Toggle Layout
 | 
			
		||||
| 
						 | 
				
			
			@ -76,7 +76,6 @@ export const ParticipantsTest = () => {
 | 
			
		|||
            <VideoTile
 | 
			
		||||
              key={item.id}
 | 
			
		||||
              name={`User ${item.id}`}
 | 
			
		||||
              showName={items.length > 2 || item.focused}
 | 
			
		||||
              disableSpeakingIndicator={items.length < 3}
 | 
			
		||||
              {...rest}
 | 
			
		||||
            />
 | 
			
		||||
| 
						 | 
				
			
			@ -14,20 +14,46 @@ See the License for the specific language governing permissions and
 | 
			
		|||
limitations under the License.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
import React, { useCallback, useEffect, useRef, useState } from "react";
 | 
			
		||||
import { useDrag, useGesture } from "@use-gesture/react";
 | 
			
		||||
import { useSprings } from "@react-spring/web";
 | 
			
		||||
import React, { Key, useCallback, useEffect, useRef, useState } from "react";
 | 
			
		||||
import { FullGestureState, useDrag, useGesture } from "@use-gesture/react";
 | 
			
		||||
import { Interpolation, SpringValue, useSprings } from "@react-spring/web";
 | 
			
		||||
import useMeasure from "react-use-measure";
 | 
			
		||||
import { ResizeObserver } from "@juggle/resize-observer";
 | 
			
		||||
import styles from "./VideoGrid.module.css";
 | 
			
		||||
import { ReactDOMAttributes } from "@use-gesture/react/dist/declarations/src/types";
 | 
			
		||||
 | 
			
		||||
export function useVideoGridLayout(hasScreenshareFeeds) {
 | 
			
		||||
  const layoutRef = useRef("freedom");
 | 
			
		||||
  const revertLayoutRef = useRef("freedom");
 | 
			
		||||
import styles from "./VideoGrid.module.css";
 | 
			
		||||
import { Layout } from "../room/GridLayoutMenu";
 | 
			
		||||
import { Participant } from "../room/InCallView";
 | 
			
		||||
 | 
			
		||||
interface TilePosition {
 | 
			
		||||
  x: number;
 | 
			
		||||
  y: number;
 | 
			
		||||
  width: number;
 | 
			
		||||
  height: number;
 | 
			
		||||
  zIndex: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface Tile {
 | 
			
		||||
  key: Key;
 | 
			
		||||
  order: number;
 | 
			
		||||
  item: Participant;
 | 
			
		||||
  remove: boolean;
 | 
			
		||||
  focused: boolean;
 | 
			
		||||
  presenter: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type LayoutDirection = "vertical" | "horizontal";
 | 
			
		||||
 | 
			
		||||
export function useVideoGridLayout(hasScreenshareFeeds: boolean): {
 | 
			
		||||
  layout: Layout;
 | 
			
		||||
  setLayout: (layout: Layout) => void;
 | 
			
		||||
} {
 | 
			
		||||
  const layoutRef = useRef<Layout>("freedom");
 | 
			
		||||
  const revertLayoutRef = useRef<Layout>("freedom");
 | 
			
		||||
  const prevHasScreenshareFeeds = useRef(hasScreenshareFeeds);
 | 
			
		||||
  const [, forceUpdate] = useState({});
 | 
			
		||||
 | 
			
		||||
  const setLayout = useCallback((layout) => {
 | 
			
		||||
  const setLayout = useCallback((layout: Layout) => {
 | 
			
		||||
    // Store the user's set layout to revert to after a screenshare is finished
 | 
			
		||||
    revertLayoutRef.current = layout;
 | 
			
		||||
    layoutRef.current = layout;
 | 
			
		||||
| 
						 | 
				
			
			@ -48,13 +74,13 @@ export function useVideoGridLayout(hasScreenshareFeeds) {
 | 
			
		|||
 | 
			
		||||
  prevHasScreenshareFeeds.current = hasScreenshareFeeds;
 | 
			
		||||
 | 
			
		||||
  return [layoutRef.current, setLayout];
 | 
			
		||||
  return { layout: layoutRef.current, setLayout };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const GAP = 8;
 | 
			
		||||
 | 
			
		||||
function useIsMounted() {
 | 
			
		||||
  const isMountedRef = useRef(false);
 | 
			
		||||
  const isMountedRef = useRef<boolean>(false);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    isMountedRef.current = true;
 | 
			
		||||
| 
						 | 
				
			
			@ -67,7 +93,7 @@ function useIsMounted() {
 | 
			
		|||
  return isMountedRef;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function isInside([x, y], targetTile) {
 | 
			
		||||
function isInside([x, y]: number[], targetTile: TilePosition): boolean {
 | 
			
		||||
  const left = targetTile.x;
 | 
			
		||||
  const top = targetTile.y;
 | 
			
		||||
  const bottom = targetTile.y + targetTile.height;
 | 
			
		||||
| 
						 | 
				
			
			@ -80,17 +106,18 @@ function isInside([x, y], targetTile) {
 | 
			
		|||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const getPipGap = (gridAspectRatio) => (gridAspectRatio < 1 ? 12 : 24);
 | 
			
		||||
const getPipGap = (gridAspectRatio: number): number =>
 | 
			
		||||
  gridAspectRatio < 1 ? 12 : 24;
 | 
			
		||||
 | 
			
		||||
function getTilePositions(
 | 
			
		||||
  tileCount,
 | 
			
		||||
  presenterTileCount,
 | 
			
		||||
  gridWidth,
 | 
			
		||||
  gridHeight,
 | 
			
		||||
  pipXRatio,
 | 
			
		||||
  pipYRatio,
 | 
			
		||||
  layout
 | 
			
		||||
) {
 | 
			
		||||
  tileCount: number,
 | 
			
		||||
  presenterTileCount: number,
 | 
			
		||||
  gridWidth: number,
 | 
			
		||||
  gridHeight: number,
 | 
			
		||||
  pipXRatio: number,
 | 
			
		||||
  pipYRatio: number,
 | 
			
		||||
  layout: Layout
 | 
			
		||||
): TilePosition[] {
 | 
			
		||||
  if (layout === "freedom") {
 | 
			
		||||
    if (tileCount === 2 && presenterTileCount === 0) {
 | 
			
		||||
      return getOneOnOneLayoutTilePositions(
 | 
			
		||||
| 
						 | 
				
			
			@ -113,11 +140,11 @@ function getTilePositions(
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
function getOneOnOneLayoutTilePositions(
 | 
			
		||||
  gridWidth,
 | 
			
		||||
  gridHeight,
 | 
			
		||||
  pipXRatio,
 | 
			
		||||
  pipYRatio
 | 
			
		||||
) {
 | 
			
		||||
  gridWidth: number,
 | 
			
		||||
  gridHeight: number,
 | 
			
		||||
  pipXRatio: number,
 | 
			
		||||
  pipYRatio: number
 | 
			
		||||
): TilePosition[] {
 | 
			
		||||
  const [remotePosition] = getFreedomLayoutTilePositions(
 | 
			
		||||
    1,
 | 
			
		||||
    0,
 | 
			
		||||
| 
						 | 
				
			
			@ -149,8 +176,12 @@ function getOneOnOneLayoutTilePositions(
 | 
			
		|||
  ];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getSpotlightLayoutTilePositions(tileCount, gridWidth, gridHeight) {
 | 
			
		||||
  const tilePositions = [];
 | 
			
		||||
function getSpotlightLayoutTilePositions(
 | 
			
		||||
  tileCount: number,
 | 
			
		||||
  gridWidth: number,
 | 
			
		||||
  gridHeight: number
 | 
			
		||||
): TilePosition[] {
 | 
			
		||||
  const tilePositions: TilePosition[] = [];
 | 
			
		||||
 | 
			
		||||
  const gridAspectRatio = gridWidth / gridHeight;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -215,11 +246,11 @@ function getSpotlightLayoutTilePositions(tileCount, gridWidth, gridHeight) {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
function getFreedomLayoutTilePositions(
 | 
			
		||||
  tileCount,
 | 
			
		||||
  presenterTileCount,
 | 
			
		||||
  gridWidth,
 | 
			
		||||
  gridHeight
 | 
			
		||||
) {
 | 
			
		||||
  tileCount: number,
 | 
			
		||||
  presenterTileCount: number,
 | 
			
		||||
  gridWidth: number,
 | 
			
		||||
  gridHeight: number
 | 
			
		||||
): TilePosition[] {
 | 
			
		||||
  if (tileCount === 0) {
 | 
			
		||||
    return [];
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -330,7 +361,14 @@ function getFreedomLayoutTilePositions(
 | 
			
		|||
  return tilePositions;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getSubGridBoundingBox(positions) {
 | 
			
		||||
function getSubGridBoundingBox(positions: TilePosition[]): {
 | 
			
		||||
  left: number;
 | 
			
		||||
  right: number;
 | 
			
		||||
  top: number;
 | 
			
		||||
  bottom: number;
 | 
			
		||||
  width: number;
 | 
			
		||||
  height: number;
 | 
			
		||||
} {
 | 
			
		||||
  let left = 0;
 | 
			
		||||
  let right = 0;
 | 
			
		||||
  let top = 0;
 | 
			
		||||
| 
						 | 
				
			
			@ -373,13 +411,18 @@ function getSubGridBoundingBox(positions) {
 | 
			
		|||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function isMobileBreakpoint(gridWidth, gridHeight) {
 | 
			
		||||
function isMobileBreakpoint(gridWidth: number, gridHeight: number): boolean {
 | 
			
		||||
  const gridAspectRatio = gridWidth / gridHeight;
 | 
			
		||||
  return gridAspectRatio < 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getGridLayout(tileCount, presenterTileCount, gridWidth, gridHeight) {
 | 
			
		||||
  let layoutDirection = "horizontal";
 | 
			
		||||
function getGridLayout(
 | 
			
		||||
  tileCount: number,
 | 
			
		||||
  presenterTileCount: number,
 | 
			
		||||
  gridWidth: number,
 | 
			
		||||
  gridHeight: number
 | 
			
		||||
): { itemGridRatio: number; layoutDirection: LayoutDirection } {
 | 
			
		||||
  let layoutDirection: LayoutDirection = "horizontal";
 | 
			
		||||
  let itemGridRatio = 1;
 | 
			
		||||
 | 
			
		||||
  if (presenterTileCount === 0) {
 | 
			
		||||
| 
						 | 
				
			
			@ -397,7 +440,13 @@ function getGridLayout(tileCount, presenterTileCount, gridWidth, gridHeight) {
 | 
			
		|||
  return { itemGridRatio, layoutDirection };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function centerTiles(positions, gridWidth, gridHeight, offsetLeft, offsetTop) {
 | 
			
		||||
function centerTiles(
 | 
			
		||||
  positions: TilePosition[],
 | 
			
		||||
  gridWidth: number,
 | 
			
		||||
  gridHeight: number,
 | 
			
		||||
  offsetLeft: number,
 | 
			
		||||
  offsetTop: number
 | 
			
		||||
) {
 | 
			
		||||
  const bounds = getSubGridBoundingBox(positions);
 | 
			
		||||
 | 
			
		||||
  const leftOffset = Math.round((gridWidth - bounds.width) / 2) + offsetLeft;
 | 
			
		||||
| 
						 | 
				
			
			@ -408,7 +457,11 @@ function centerTiles(positions, gridWidth, gridHeight, offsetLeft, offsetTop) {
 | 
			
		|||
  return positions;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function applyTileOffsets(positions, leftOffset, topOffset) {
 | 
			
		||||
function applyTileOffsets(
 | 
			
		||||
  positions: TilePosition[],
 | 
			
		||||
  leftOffset: number,
 | 
			
		||||
  topOffset: number
 | 
			
		||||
) {
 | 
			
		||||
  for (const position of positions) {
 | 
			
		||||
    position.x += leftOffset;
 | 
			
		||||
    position.y += topOffset;
 | 
			
		||||
| 
						 | 
				
			
			@ -417,12 +470,16 @@ function applyTileOffsets(positions, leftOffset, topOffset) {
 | 
			
		|||
  return positions;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getSubGridLayout(tileCount, gridWidth, gridHeight) {
 | 
			
		||||
function getSubGridLayout(
 | 
			
		||||
  tileCount: number,
 | 
			
		||||
  gridWidth: number,
 | 
			
		||||
  gridHeight: number
 | 
			
		||||
): { columnCount: number; rowCount: number; tileAspectRatio: number } {
 | 
			
		||||
  const gridAspectRatio = gridWidth / gridHeight;
 | 
			
		||||
 | 
			
		||||
  let columnCount;
 | 
			
		||||
  let rowCount;
 | 
			
		||||
  let tileAspectRatio = 16 / 9;
 | 
			
		||||
  let columnCount: number;
 | 
			
		||||
  let rowCount: number;
 | 
			
		||||
  let tileAspectRatio: number = 16 / 9;
 | 
			
		||||
 | 
			
		||||
  if (gridAspectRatio < 3 / 4) {
 | 
			
		||||
    // Phone
 | 
			
		||||
| 
						 | 
				
			
			@ -528,26 +585,26 @@ function getSubGridLayout(tileCount, gridWidth, gridHeight) {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
function getSubGridPositions(
 | 
			
		||||
  tileCount,
 | 
			
		||||
  columnCount,
 | 
			
		||||
  rowCount,
 | 
			
		||||
  tileAspectRatio,
 | 
			
		||||
  gridWidth,
 | 
			
		||||
  gridHeight
 | 
			
		||||
  tileCount: number,
 | 
			
		||||
  columnCount: number,
 | 
			
		||||
  rowCount: number,
 | 
			
		||||
  tileAspectRatio: number,
 | 
			
		||||
  gridWidth: number,
 | 
			
		||||
  gridHeight: number
 | 
			
		||||
) {
 | 
			
		||||
  if (tileCount === 0) {
 | 
			
		||||
    return [];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const newTilePositions = [];
 | 
			
		||||
  const newTilePositions: TilePosition[] = [];
 | 
			
		||||
 | 
			
		||||
  const boxWidth = Math.round(
 | 
			
		||||
    (gridWidth - GAP * (columnCount + 1)) / columnCount
 | 
			
		||||
  );
 | 
			
		||||
  const boxHeight = Math.round((gridHeight - GAP * (rowCount + 1)) / rowCount);
 | 
			
		||||
 | 
			
		||||
  let tileWidth;
 | 
			
		||||
  let tileHeight;
 | 
			
		||||
  let tileWidth: number;
 | 
			
		||||
  let tileHeight: number;
 | 
			
		||||
 | 
			
		||||
  if (tileAspectRatio) {
 | 
			
		||||
    const boxAspectRatio = boxWidth / boxHeight;
 | 
			
		||||
| 
						 | 
				
			
			@ -568,7 +625,7 @@ function getSubGridPositions(
 | 
			
		|||
    const verticalIndex = Math.floor(i / columnCount);
 | 
			
		||||
    const top = verticalIndex * GAP + verticalIndex * tileHeight;
 | 
			
		||||
 | 
			
		||||
    let rowItemCount;
 | 
			
		||||
    let rowItemCount: number;
 | 
			
		||||
 | 
			
		||||
    if (verticalIndex + 1 === rowCount && tileCount % columnCount !== 0) {
 | 
			
		||||
      rowItemCount = tileCount % columnCount;
 | 
			
		||||
| 
						 | 
				
			
			@ -603,16 +660,16 @@ function getSubGridPositions(
 | 
			
		|||
  return newTilePositions;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function reorderTiles(tiles, layout) {
 | 
			
		||||
function reorderTiles(tiles: Tile[], layout: Layout) {
 | 
			
		||||
  if (layout === "freedom" && tiles.length === 2) {
 | 
			
		||||
    // 1:1 layout
 | 
			
		||||
    tiles.forEach((tile) => (tile.order = tile.item.isLocal ? 0 : 1));
 | 
			
		||||
  } else {
 | 
			
		||||
    const focusedTiles = [];
 | 
			
		||||
    const presenterTiles = [];
 | 
			
		||||
    const otherTiles = [];
 | 
			
		||||
    const focusedTiles: Tile[] = [];
 | 
			
		||||
    const presenterTiles: Tile[] = [];
 | 
			
		||||
    const otherTiles: Tile[] = [];
 | 
			
		||||
 | 
			
		||||
    const orderedTiles = new Array(tiles.length);
 | 
			
		||||
    const orderedTiles: Tile[] = new Array(tiles.length);
 | 
			
		||||
    tiles.forEach((tile) => (orderedTiles[tile.order] = tile));
 | 
			
		||||
 | 
			
		||||
    orderedTiles.forEach((tile) =>
 | 
			
		||||
| 
						 | 
				
			
			@ -630,27 +687,63 @@ function reorderTiles(tiles, layout) {
 | 
			
		|||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function VideoGrid({ items, layout, disableAnimations, children }) {
 | 
			
		||||
interface DragTileData {
 | 
			
		||||
  offsetX: number;
 | 
			
		||||
  offsetY: number;
 | 
			
		||||
  key: Key;
 | 
			
		||||
  x: number;
 | 
			
		||||
  y: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface ChildrenProperties extends ReactDOMAttributes {
 | 
			
		||||
  key: Key;
 | 
			
		||||
  style: {
 | 
			
		||||
    scale: SpringValue<number>;
 | 
			
		||||
    opacity: SpringValue<number>;
 | 
			
		||||
    boxShadow: Interpolation<number, string>;
 | 
			
		||||
  };
 | 
			
		||||
  width: number;
 | 
			
		||||
  height: number;
 | 
			
		||||
  item: Participant;
 | 
			
		||||
  [index: string]: unknown;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface VideoGridProps {
 | 
			
		||||
  items: Participant[];
 | 
			
		||||
  layout: Layout;
 | 
			
		||||
  disableAnimations?: boolean;
 | 
			
		||||
  children: (props: ChildrenProperties) => React.ReactNode;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function VideoGrid({
 | 
			
		||||
  items,
 | 
			
		||||
  layout,
 | 
			
		||||
  disableAnimations,
 | 
			
		||||
  children,
 | 
			
		||||
}: VideoGridProps) {
 | 
			
		||||
  // Place the PiP in the bottom right corner by default
 | 
			
		||||
  const [pipXRatio, setPipXRatio] = useState(1);
 | 
			
		||||
  const [pipYRatio, setPipYRatio] = useState(1);
 | 
			
		||||
 | 
			
		||||
  const [{ tiles, tilePositions }, setTileState] = useState({
 | 
			
		||||
  const [{ tiles, tilePositions }, setTileState] = useState<{
 | 
			
		||||
    tiles: Tile[];
 | 
			
		||||
    tilePositions: TilePosition[];
 | 
			
		||||
  }>({
 | 
			
		||||
    tiles: [],
 | 
			
		||||
    tilePositions: [],
 | 
			
		||||
  });
 | 
			
		||||
  const [scrollPosition, setScrollPosition] = useState(0);
 | 
			
		||||
  const draggingTileRef = useRef(null);
 | 
			
		||||
  const lastTappedRef = useRef({});
 | 
			
		||||
  const lastLayoutRef = useRef(layout);
 | 
			
		||||
  const [scrollPosition, setScrollPosition] = useState<number>(0);
 | 
			
		||||
  const draggingTileRef = useRef<DragTileData>(null);
 | 
			
		||||
  const lastTappedRef = useRef<{ [index: Key]: number }>({});
 | 
			
		||||
  const lastLayoutRef = useRef<Layout>(layout);
 | 
			
		||||
  const isMounted = useIsMounted();
 | 
			
		||||
 | 
			
		||||
  const [gridRef, gridBounds] = useMeasure({ polyfill: ResizeObserver });
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setTileState(({ tiles, ...rest }) => {
 | 
			
		||||
      const newTiles = [];
 | 
			
		||||
      const removedTileKeys = new Set();
 | 
			
		||||
      const newTiles: Tile[] = [];
 | 
			
		||||
      const removedTileKeys: Set<Key> = new Set();
 | 
			
		||||
 | 
			
		||||
      for (const tile of tiles) {
 | 
			
		||||
        let item = items.find((item) => item.id === tile.key);
 | 
			
		||||
| 
						 | 
				
			
			@ -663,7 +756,7 @@ export function VideoGrid({ items, layout, disableAnimations, children }) {
 | 
			
		|||
          removedTileKeys.add(tile.key);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let focused;
 | 
			
		||||
        let focused: boolean;
 | 
			
		||||
        let presenter = false;
 | 
			
		||||
 | 
			
		||||
        if (layout === "spotlight") {
 | 
			
		||||
| 
						 | 
				
			
			@ -694,7 +787,7 @@ export function VideoGrid({ items, layout, disableAnimations, children }) {
 | 
			
		|||
          continue;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const newTile = {
 | 
			
		||||
        const newTile: Tile = {
 | 
			
		||||
          key: item.id,
 | 
			
		||||
          order: existingTile?.order ?? newTiles.length,
 | 
			
		||||
          item,
 | 
			
		||||
| 
						 | 
				
			
			@ -721,7 +814,7 @@ export function VideoGrid({ items, layout, disableAnimations, children }) {
 | 
			
		|||
          }
 | 
			
		||||
 | 
			
		||||
          setTileState(({ tiles, ...rest }) => {
 | 
			
		||||
            const newTiles = tiles
 | 
			
		||||
            const newTiles: Tile[] = tiles
 | 
			
		||||
              .filter((tile) => !removedTileKeys.has(tile.key))
 | 
			
		||||
              .map((tile) => ({ ...tile })); // clone before reordering
 | 
			
		||||
            reorderTiles(newTiles, layout);
 | 
			
		||||
| 
						 | 
				
			
			@ -772,7 +865,7 @@ export function VideoGrid({ items, layout, disableAnimations, children }) {
 | 
			
		|||
  }, [items, gridBounds, layout, isMounted, pipXRatio, pipYRatio]);
 | 
			
		||||
 | 
			
		||||
  const animate = useCallback(
 | 
			
		||||
    (tiles) => (tileIndex) => {
 | 
			
		||||
    (tiles: Tile[]) => (tileIndex: number) => {
 | 
			
		||||
      const tile = tiles[tileIndex];
 | 
			
		||||
      const tilePosition = tilePositions[tile.order];
 | 
			
		||||
      const draggingTile = draggingTileRef.current;
 | 
			
		||||
| 
						 | 
				
			
			@ -789,7 +882,7 @@ export function VideoGrid({ items, layout, disableAnimations, children }) {
 | 
			
		|||
          opacity: 1,
 | 
			
		||||
          zIndex: 2,
 | 
			
		||||
          shadow: 15,
 | 
			
		||||
          immediate: (key) =>
 | 
			
		||||
          immediate: (key: string) =>
 | 
			
		||||
            disableAnimations ||
 | 
			
		||||
            key === "zIndex" ||
 | 
			
		||||
            key === "x" ||
 | 
			
		||||
| 
						 | 
				
			
			@ -831,11 +924,11 @@ export function VideoGrid({ items, layout, disableAnimations, children }) {
 | 
			
		|||
            opacity: 0,
 | 
			
		||||
          },
 | 
			
		||||
          reset: false,
 | 
			
		||||
          immediate: (key) =>
 | 
			
		||||
          immediate: (key: string) =>
 | 
			
		||||
            disableAnimations || key === "zIndex" || key === "shadow",
 | 
			
		||||
          // If we just stopped dragging a tile, give it time for its animation
 | 
			
		||||
          // to settle before pushing its z-index back down
 | 
			
		||||
          delay: (key) => (key === "zIndex" ? 500 : 0),
 | 
			
		||||
          delay: (key: string) => (key === "zIndex" ? 500 : 0),
 | 
			
		||||
        };
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
| 
						 | 
				
			
			@ -849,7 +942,7 @@ export function VideoGrid({ items, layout, disableAnimations, children }) {
 | 
			
		|||
  ]);
 | 
			
		||||
 | 
			
		||||
  const onTap = useCallback(
 | 
			
		||||
    (tileKey) => {
 | 
			
		||||
    (tileKey: Key) => {
 | 
			
		||||
      const lastTapped = lastTappedRef.current[tileKey];
 | 
			
		||||
 | 
			
		||||
      if (!lastTapped || Date.now() - lastTapped > 500) {
 | 
			
		||||
| 
						 | 
				
			
			@ -866,7 +959,7 @@ export function VideoGrid({ items, layout, disableAnimations, children }) {
 | 
			
		|||
      setTileState(({ tiles, ...state }) => {
 | 
			
		||||
        let presenterTileCount = 0;
 | 
			
		||||
        const newTiles = tiles.map((tile) => {
 | 
			
		||||
          let newTile = { ...tile }; // clone before reordering
 | 
			
		||||
          const newTile = { ...tile }; // clone before reordering
 | 
			
		||||
 | 
			
		||||
          if (tile.item === item) {
 | 
			
		||||
            newTile.focused = !tile.focused;
 | 
			
		||||
| 
						 | 
				
			
			@ -895,7 +988,7 @@ export function VideoGrid({ items, layout, disableAnimations, children }) {
 | 
			
		|||
        };
 | 
			
		||||
      });
 | 
			
		||||
    },
 | 
			
		||||
    [tiles, gridBounds, layout]
 | 
			
		||||
    [tiles, layout, gridBounds.width, gridBounds.height, pipXRatio, pipYRatio]
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const bindTile = useDrag(
 | 
			
		||||
| 
						 | 
				
			
			@ -1008,12 +1101,18 @@ export function VideoGrid({ items, layout, disableAnimations, children }) {
 | 
			
		|||
  );
 | 
			
		||||
 | 
			
		||||
  const onGridGesture = useCallback(
 | 
			
		||||
    (e, isWheel) => {
 | 
			
		||||
    (
 | 
			
		||||
      e:
 | 
			
		||||
        | Omit<FullGestureState<"wheel">, "event">
 | 
			
		||||
        | Omit<FullGestureState<"drag">, "event">,
 | 
			
		||||
      isWheel: boolean
 | 
			
		||||
    ) => {
 | 
			
		||||
      if (layout !== "spotlight") {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const isMobile = isMobileBreakpoint(gridBounds.width, gridBounds.height);
 | 
			
		||||
 | 
			
		||||
      let movement = e.delta[isMobile ? 0 : 1];
 | 
			
		||||
 | 
			
		||||
      if (isWheel) {
 | 
			
		||||
| 
						 | 
				
			
			@ -17,30 +17,49 @@ limitations under the License.
 | 
			
		|||
import React, { forwardRef } from "react";
 | 
			
		||||
import { animated } from "@react-spring/web";
 | 
			
		||||
import classNames from "classnames";
 | 
			
		||||
 | 
			
		||||
import styles from "./VideoTile.module.css";
 | 
			
		||||
import { ReactComponent as MicMutedIcon } from "../icons/MicMuted.svg";
 | 
			
		||||
import { ReactComponent as VideoMutedIcon } from "../icons/VideoMuted.svg";
 | 
			
		||||
import { AudioButton, FullscreenButton } from "../button/Button";
 | 
			
		||||
 | 
			
		||||
export const VideoTile = forwardRef(
 | 
			
		||||
interface Props {
 | 
			
		||||
  name: string;
 | 
			
		||||
  speaking?: boolean;
 | 
			
		||||
  audioMuted?: boolean;
 | 
			
		||||
  videoMuted?: boolean;
 | 
			
		||||
  screenshare?: boolean;
 | 
			
		||||
  avatar?: JSX.Element;
 | 
			
		||||
  mediaRef?: React.RefObject<MediaElement>;
 | 
			
		||||
  onOptionsPress?: () => void;
 | 
			
		||||
  localVolume?: number;
 | 
			
		||||
  isFullscreen?: boolean;
 | 
			
		||||
  onFullscreen?: () => void;
 | 
			
		||||
  className?: string;
 | 
			
		||||
  showOptions?: boolean;
 | 
			
		||||
  isLocal?: boolean;
 | 
			
		||||
  disableSpeakingIndicator?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const VideoTile = forwardRef<HTMLDivElement, Props>(
 | 
			
		||||
  (
 | 
			
		||||
    {
 | 
			
		||||
      className,
 | 
			
		||||
      isLocal,
 | 
			
		||||
      name,
 | 
			
		||||
      speaking,
 | 
			
		||||
      audioMuted,
 | 
			
		||||
      noVideo,
 | 
			
		||||
      videoMuted,
 | 
			
		||||
      screenshare,
 | 
			
		||||
      avatar,
 | 
			
		||||
      name,
 | 
			
		||||
      showName,
 | 
			
		||||
      mediaRef,
 | 
			
		||||
      onOptionsPress,
 | 
			
		||||
      showOptions,
 | 
			
		||||
      localVolume,
 | 
			
		||||
      isFullscreen,
 | 
			
		||||
      onFullscreen,
 | 
			
		||||
      className,
 | 
			
		||||
      showOptions,
 | 
			
		||||
      isLocal,
 | 
			
		||||
      // TODO: disableSpeakingIndicator is not used atm.
 | 
			
		||||
      disableSpeakingIndicator,
 | 
			
		||||
      ...rest
 | 
			
		||||
    },
 | 
			
		||||
    ref
 | 
			
		||||
| 
						 | 
				
			
			@ -75,7 +94,7 @@ export const VideoTile = forwardRef(
 | 
			
		|||
            )}
 | 
			
		||||
          </div>
 | 
			
		||||
        )}
 | 
			
		||||
        {(videoMuted || noVideo) && (
 | 
			
		||||
        {videoMuted && (
 | 
			
		||||
          <>
 | 
			
		||||
            <div className={styles.videoMutedOverlay} />
 | 
			
		||||
            {avatar}
 | 
			
		||||
| 
						 | 
				
			
			@ -86,15 +105,12 @@ export const VideoTile = forwardRef(
 | 
			
		|||
            <span>{`${name} is presenting`}</span>
 | 
			
		||||
          </div>
 | 
			
		||||
        ) : (
 | 
			
		||||
          (showName || audioMuted || (videoMuted && !noVideo)) && (
 | 
			
		||||
            <div className={classNames(styles.infoBubble, styles.memberName)}>
 | 
			
		||||
              {audioMuted && !(videoMuted && !noVideo) && <MicMutedIcon />}
 | 
			
		||||
              {videoMuted && !noVideo && <VideoMutedIcon />}
 | 
			
		||||
              {showName && <span title={name}>{name}</span>}
 | 
			
		||||
            </div>
 | 
			
		||||
          )
 | 
			
		||||
          <div className={classNames(styles.infoBubble, styles.memberName)}>
 | 
			
		||||
            {audioMuted && !videoMuted && <MicMutedIcon />}
 | 
			
		||||
            {videoMuted && <VideoMutedIcon />}
 | 
			
		||||
            <span title={name}>{name}</span>
 | 
			
		||||
          </div>
 | 
			
		||||
        )}
 | 
			
		||||
 | 
			
		||||
        <video ref={mediaRef} playsInline disablePictureInPicture />
 | 
			
		||||
      </animated.div>
 | 
			
		||||
    );
 | 
			
		||||
| 
						 | 
				
			
			@ -16,33 +16,51 @@ limitations under the License.
 | 
			
		|||
 | 
			
		||||
import { SDPStreamMetadataPurpose } from "matrix-js-sdk/src/webrtc/callEventTypes";
 | 
			
		||||
import React from "react";
 | 
			
		||||
import { useCallback } from "react";
 | 
			
		||||
import { RoomMember } from "matrix-js-sdk";
 | 
			
		||||
 | 
			
		||||
import { useCallFeed } from "./useCallFeed";
 | 
			
		||||
import { useSpatialMediaStream } from "./useMediaStream";
 | 
			
		||||
import { useRoomMemberName } from "./useRoomMemberName";
 | 
			
		||||
import { VideoTile } from "./VideoTile";
 | 
			
		||||
import { VideoTileSettingsModal } from "./VideoTileSettingsModal";
 | 
			
		||||
import { useModalTriggerState } from "../Modal";
 | 
			
		||||
import { useCallback } from "react";
 | 
			
		||||
import { Participant } from "../room/InCallView";
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  item: Participant;
 | 
			
		||||
  width?: number;
 | 
			
		||||
  height?: number;
 | 
			
		||||
  getAvatar: (
 | 
			
		||||
    roomMember: RoomMember,
 | 
			
		||||
    width: number,
 | 
			
		||||
    height: number
 | 
			
		||||
  ) => JSX.Element;
 | 
			
		||||
  audioOutputDevice: string;
 | 
			
		||||
  audioContext: AudioContext;
 | 
			
		||||
  audioDestination: AudioNode;
 | 
			
		||||
  disableSpeakingIndicator: boolean;
 | 
			
		||||
  isFullscreen: boolean;
 | 
			
		||||
  onFullscreen: (item: Participant) => void;
 | 
			
		||||
}
 | 
			
		||||
export function VideoTileContainer({
 | 
			
		||||
  item,
 | 
			
		||||
  width,
 | 
			
		||||
  height,
 | 
			
		||||
  getAvatar,
 | 
			
		||||
  showName,
 | 
			
		||||
  audioOutputDevice,
 | 
			
		||||
  audioContext,
 | 
			
		||||
  audioDestination,
 | 
			
		||||
  disableSpeakingIndicator,
 | 
			
		||||
  isFullscreen,
 | 
			
		||||
  onFullscreen,
 | 
			
		||||
  ...rest
 | 
			
		||||
}) {
 | 
			
		||||
}: Props) {
 | 
			
		||||
  const {
 | 
			
		||||
    isLocal,
 | 
			
		||||
    audioMuted,
 | 
			
		||||
    videoMuted,
 | 
			
		||||
    localVolume,
 | 
			
		||||
    noVideo,
 | 
			
		||||
    speaking,
 | 
			
		||||
    stream,
 | 
			
		||||
    purpose,
 | 
			
		||||
| 
						 | 
				
			
			@ -77,11 +95,9 @@ export function VideoTileContainer({
 | 
			
		|||
        isLocal={isLocal}
 | 
			
		||||
        speaking={speaking && !disableSpeakingIndicator}
 | 
			
		||||
        audioMuted={audioMuted}
 | 
			
		||||
        noVideo={noVideo}
 | 
			
		||||
        videoMuted={videoMuted}
 | 
			
		||||
        screenshare={purpose === SDPStreamMetadataPurpose.Screenshare}
 | 
			
		||||
        name={rawDisplayName}
 | 
			
		||||
        showName={showName}
 | 
			
		||||
        ref={tileRef}
 | 
			
		||||
        mediaRef={mediaRef}
 | 
			
		||||
        avatar={getAvatar && getAvatar(member, width, height)}
 | 
			
		||||
| 
						 | 
				
			
			@ -23,6 +23,14 @@
 | 
			
		|||
  -webkit-appearance: none;
 | 
			
		||||
  appearance: none;
 | 
			
		||||
 | 
			
		||||
  background-color: transparent;
 | 
			
		||||
  --slider-color: var(--quinary-content);
 | 
			
		||||
  --slider-height: 4px;
 | 
			
		||||
  --thumb-color: var(--accent);
 | 
			
		||||
  --thumb-radius: 100%;
 | 
			
		||||
  --thumb-size: 16px;
 | 
			
		||||
  --thumb-margin-top: -6px;
 | 
			
		||||
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -31,51 +39,66 @@
 | 
			
		|||
  -moz-appearance: none;
 | 
			
		||||
  appearance: none;
 | 
			
		||||
 | 
			
		||||
  height: 4px;
 | 
			
		||||
  background-color: var(--slider-color);
 | 
			
		||||
  height: var(--slider-height);
 | 
			
		||||
}
 | 
			
		||||
.localVolumeSlider[type="range"]::-ms-track {
 | 
			
		||||
  -ms-appearance: none;
 | 
			
		||||
  appearance: none;
 | 
			
		||||
 | 
			
		||||
  height: 4px;
 | 
			
		||||
  background-color: var(--slider-color);
 | 
			
		||||
  height: var(--slider-height);
 | 
			
		||||
}
 | 
			
		||||
.localVolumeSlider[type="range"]::-webkit-slider-runnable-track {
 | 
			
		||||
  -webkit-appearance: none;
 | 
			
		||||
  appearance: none;
 | 
			
		||||
 | 
			
		||||
  height: 4px;
 | 
			
		||||
  background-color: var(--slider-color);
 | 
			
		||||
  height: var(--slider-height);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.localVolumeSlider[type="range"]::-moz-range-thumb {
 | 
			
		||||
  -moz-appearance: none;
 | 
			
		||||
  appearance: none;
 | 
			
		||||
 | 
			
		||||
  height: 16px;
 | 
			
		||||
  width: 16px;
 | 
			
		||||
  margin-top: -6px;
 | 
			
		||||
 | 
			
		||||
  border-radius: 100%;
 | 
			
		||||
  background: var(--accent);
 | 
			
		||||
  height: var(--thumb-size);
 | 
			
		||||
  width: var(--thumb-size);
 | 
			
		||||
  margin-top: var(--thumb-margin-top);
 | 
			
		||||
  border-radius: var(--thumb-radius);
 | 
			
		||||
  background: var(--thumb-color);
 | 
			
		||||
}
 | 
			
		||||
.localVolumeSlider[type="range"]::-ms-thumb {
 | 
			
		||||
  -ms-appearance: none;
 | 
			
		||||
  appearance: none;
 | 
			
		||||
 | 
			
		||||
  height: 16px;
 | 
			
		||||
  width: 16px;
 | 
			
		||||
  margin-top: -6px;
 | 
			
		||||
 | 
			
		||||
  border-radius: 100%;
 | 
			
		||||
  background: var(--accent);
 | 
			
		||||
  height: var(--thumb-size);
 | 
			
		||||
  width: var(--thumb-size);
 | 
			
		||||
  margin-top: var(--thumb-margin-top);
 | 
			
		||||
  border-radius: var(--thumb-radius);
 | 
			
		||||
  background: var(--thumb-color);
 | 
			
		||||
}
 | 
			
		||||
.localVolumeSlider[type="range"]::-webkit-slider-thumb {
 | 
			
		||||
  -webkit-appearance: none;
 | 
			
		||||
  appearance: none;
 | 
			
		||||
 | 
			
		||||
  height: 16px;
 | 
			
		||||
  width: 16px;
 | 
			
		||||
  margin-top: -6px;
 | 
			
		||||
 | 
			
		||||
  border-radius: 100%;
 | 
			
		||||
  background: var(--accent);
 | 
			
		||||
  height: var(--thumb-size);
 | 
			
		||||
  width: var(--thumb-size);
 | 
			
		||||
  margin-top: var(--thumb-margin-top);
 | 
			
		||||
  border-radius: var(--thumb-radius);
 | 
			
		||||
  background: var(--thumb-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.localVolumeSlider[type="range"]::-moz-range-progress {
 | 
			
		||||
  -moz-appearance: none;
 | 
			
		||||
  appearance: none;
 | 
			
		||||
 | 
			
		||||
  height: var(--slider-height);
 | 
			
		||||
  background: var(--thumb-color);
 | 
			
		||||
}
 | 
			
		||||
.localVolumeSlider[type="range"]::-ms-fill-lower {
 | 
			
		||||
  -moz-appearance: none;
 | 
			
		||||
  appearance: none;
 | 
			
		||||
 | 
			
		||||
  height: var(--slider-height);
 | 
			
		||||
  background: var(--thumb-color);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,16 +15,25 @@ limitations under the License.
 | 
			
		|||
*/
 | 
			
		||||
 | 
			
		||||
import { useState, useEffect } from "react";
 | 
			
		||||
import { CallFeedEvent } from "matrix-js-sdk/src/webrtc/callFeed";
 | 
			
		||||
import { CallFeed, CallFeedEvent } from "matrix-js-sdk/src/webrtc/callFeed";
 | 
			
		||||
import { RoomMember } from "matrix-js-sdk";
 | 
			
		||||
import { SDPStreamMetadataPurpose } from "matrix-js-sdk/src/webrtc/callEventTypes";
 | 
			
		||||
 | 
			
		||||
function getCallFeedState(callFeed) {
 | 
			
		||||
interface CallFeedState {
 | 
			
		||||
  member: RoomMember;
 | 
			
		||||
  isLocal: boolean;
 | 
			
		||||
  speaking: boolean;
 | 
			
		||||
  videoMuted: boolean;
 | 
			
		||||
  audioMuted: boolean;
 | 
			
		||||
  localVolume: number;
 | 
			
		||||
  stream: MediaStream;
 | 
			
		||||
  purpose: SDPStreamMetadataPurpose;
 | 
			
		||||
}
 | 
			
		||||
function getCallFeedState(callFeed: CallFeed): CallFeedState {
 | 
			
		||||
  return {
 | 
			
		||||
    member: callFeed ? callFeed.getMember() : null,
 | 
			
		||||
    isLocal: callFeed ? callFeed.isLocal() : false,
 | 
			
		||||
    speaking: callFeed ? callFeed.isSpeaking() : false,
 | 
			
		||||
    noVideo: callFeed
 | 
			
		||||
      ? !callFeed.stream || callFeed.stream.getVideoTracks().length === 0
 | 
			
		||||
      : true,
 | 
			
		||||
    videoMuted: callFeed ? callFeed.isVideoMuted() : true,
 | 
			
		||||
    audioMuted: callFeed ? callFeed.isAudioMuted() : true,
 | 
			
		||||
    localVolume: callFeed ? callFeed.getLocalVolume() : 0,
 | 
			
		||||
| 
						 | 
				
			
			@ -33,19 +42,21 @@ function getCallFeedState(callFeed) {
 | 
			
		|||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function useCallFeed(callFeed) {
 | 
			
		||||
  const [state, setState] = useState(() => getCallFeedState(callFeed));
 | 
			
		||||
export function useCallFeed(callFeed: CallFeed): CallFeedState {
 | 
			
		||||
  const [state, setState] = useState<CallFeedState>(() =>
 | 
			
		||||
    getCallFeedState(callFeed)
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    function onSpeaking(speaking) {
 | 
			
		||||
    function onSpeaking(speaking: boolean) {
 | 
			
		||||
      setState((prevState) => ({ ...prevState, speaking }));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function onMuteStateChanged(audioMuted, videoMuted) {
 | 
			
		||||
    function onMuteStateChanged(audioMuted: boolean, videoMuted: boolean) {
 | 
			
		||||
      setState((prevState) => ({ ...prevState, audioMuted, videoMuted }));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function onLocalVolumeChanged(localVolume) {
 | 
			
		||||
    function onLocalVolumeChanged(localVolume: number) {
 | 
			
		||||
      setState((prevState) => ({ ...prevState, localVolume }));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -181,8 +181,8 @@ export const useSpatialMediaStream = (
 | 
			
		|||
  audioDestination: AudioNode,
 | 
			
		||||
  mute = false,
 | 
			
		||||
  localVolume?: number
 | 
			
		||||
): [RefObject<Element>, RefObject<MediaElement>] => {
 | 
			
		||||
  const tileRef = useRef<Element>();
 | 
			
		||||
): [RefObject<HTMLDivElement>, RefObject<MediaElement>] => {
 | 
			
		||||
  const tileRef = useRef<HTMLDivElement>();
 | 
			
		||||
  const [spatialAudio] = useSpatialAudio();
 | 
			
		||||
  // We always handle audio separately form the video element
 | 
			
		||||
  const mediaRef = useMediaStream(stream, undefined, true, undefined);
 | 
			
		||||
| 
						 | 
				
			
			@ -192,13 +192,7 @@ export const useSpatialMediaStream = (
 | 
			
		|||
  const sourceRef = useRef<MediaStreamAudioSourceNode>();
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (
 | 
			
		||||
      spatialAudio &&
 | 
			
		||||
      audioContext &&
 | 
			
		||||
      tileRef.current &&
 | 
			
		||||
      !mute &&
 | 
			
		||||
      stream.getAudioTracks().length > 0
 | 
			
		||||
    ) {
 | 
			
		||||
    if (spatialAudio && audioContext && tileRef.current && !mute) {
 | 
			
		||||
      if (!pannerNodeRef.current) {
 | 
			
		||||
        pannerNodeRef.current = new PannerNode(audioContext, {
 | 
			
		||||
          panningModel: "HRTF",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,10 +14,15 @@ See the License for the specific language governing permissions and
 | 
			
		|||
limitations under the License.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
import { RoomMember, RoomMemberEvent } from "matrix-js-sdk";
 | 
			
		||||
import { useState, useEffect } from "react";
 | 
			
		||||
 | 
			
		||||
export function useRoomMemberName(member) {
 | 
			
		||||
  const [state, setState] = useState({
 | 
			
		||||
interface RoomMemberName {
 | 
			
		||||
  name: string;
 | 
			
		||||
  rawDisplayName: string;
 | 
			
		||||
}
 | 
			
		||||
export function useRoomMemberName(member: RoomMember): RoomMemberName {
 | 
			
		||||
  const [state, setState] = useState<RoomMemberName>({
 | 
			
		||||
    name: member.name,
 | 
			
		||||
    rawDisplayName: member.rawDisplayName,
 | 
			
		||||
  });
 | 
			
		||||
| 
						 | 
				
			
			@ -29,10 +34,10 @@ export function useRoomMemberName(member) {
 | 
			
		|||
 | 
			
		||||
    updateName();
 | 
			
		||||
 | 
			
		||||
    member.on("RoomMember.name", updateName);
 | 
			
		||||
    member.on(RoomMemberEvent.Name, updateName);
 | 
			
		||||
 | 
			
		||||
    return () => {
 | 
			
		||||
      member.removeListener("RoomMember.name", updateName);
 | 
			
		||||
      member.removeListener(RoomMemberEvent.Name, updateName);
 | 
			
		||||
    };
 | 
			
		||||
  }, [member]);
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue