Merge branch 'livekit-experiment' into livekit-load-test
This commit is contained in:
		
				commit
				
					
						6436e66adb
					
				
			
		
					 126 changed files with 6789 additions and 1444 deletions
				
			
		| 
						 | 
				
			
			@ -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.
 | 
			
		||||
| 
						 | 
				
			
			@ -25,15 +25,24 @@ import {
 | 
			
		|||
import { usePreventScroll } from "@react-aria/overlays";
 | 
			
		||||
import classNames from "classnames";
 | 
			
		||||
import { Room, Track } from "livekit-client";
 | 
			
		||||
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
 | 
			
		||||
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 { useTranslation } from "react-i18next";
 | 
			
		||||
import useMeasure from "react-use-measure";
 | 
			
		||||
import { OverlayTriggerState } from "@react-stately/overlays";
 | 
			
		||||
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
 | 
			
		||||
 | 
			
		||||
import type { IWidgetApiRequest } from "matrix-widget-api";
 | 
			
		||||
import {
 | 
			
		||||
  HangupButton,
 | 
			
		||||
  MicButton,
 | 
			
		||||
  VideoButton,
 | 
			
		||||
  ScreenshareButton,
 | 
			
		||||
  SettingsButton,
 | 
			
		||||
  InviteButton,
 | 
			
		||||
} from "../button";
 | 
			
		||||
import {
 | 
			
		||||
  Header,
 | 
			
		||||
  LeftNav,
 | 
			
		||||
| 
						 | 
				
			
			@ -41,38 +50,34 @@ import {
 | 
			
		|||
  RoomHeaderInfo,
 | 
			
		||||
  VersionMismatchWarning,
 | 
			
		||||
} from "../Header";
 | 
			
		||||
import { useModalTriggerState } from "../Modal";
 | 
			
		||||
import { PosthogAnalytics } from "../PosthogAnalytics";
 | 
			
		||||
import { useUrlParams } from "../UrlParams";
 | 
			
		||||
import { UserMenuContainer } from "../UserMenuContainer";
 | 
			
		||||
import {
 | 
			
		||||
  HangupButton,
 | 
			
		||||
  MicButton,
 | 
			
		||||
  ScreenshareButton,
 | 
			
		||||
  VideoButton,
 | 
			
		||||
} from "../button";
 | 
			
		||||
import { MediaDevicesState } from "../settings/mediaDevices";
 | 
			
		||||
import { useRageshakeRequestModal } from "../settings/submit-rageshake";
 | 
			
		||||
import { useShowInspector } from "../settings/useSetting";
 | 
			
		||||
import { useCallViewKeyboardShortcuts } from "../useCallViewKeyboardShortcuts";
 | 
			
		||||
import { usePrefersReducedMotion } from "../usePrefersReducedMotion";
 | 
			
		||||
import {
 | 
			
		||||
  TileDescriptor,
 | 
			
		||||
  VideoGrid,
 | 
			
		||||
  useVideoGridLayout,
 | 
			
		||||
  TileDescriptor,
 | 
			
		||||
} from "../video-grid/VideoGrid";
 | 
			
		||||
import { useNewGrid, 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 { OverflowMenu } from "./OverflowMenu";
 | 
			
		||||
import { RageshakeRequestModal } from "./RageshakeRequestModal";
 | 
			
		||||
import { MatrixInfo } from "./VideoPreview";
 | 
			
		||||
import { useJoinRule } from "./useJoinRule";
 | 
			
		||||
import { ParticipantInfo } from "./useGroupCall";
 | 
			
		||||
import { 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";
 | 
			
		||||
 | 
			
		||||
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
 | 
			
		||||
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
 | 
			
		||||
| 
						 | 
				
			
			@ -80,6 +85,11 @@ const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
 | 
			
		|||
// For now we can disable screensharing in Safari.
 | 
			
		||||
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
 | 
			
		||||
 | 
			
		||||
interface LocalUserChoices {
 | 
			
		||||
  videoMuted: boolean;
 | 
			
		||||
  audioMuted: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  client: MatrixClient;
 | 
			
		||||
  groupCall: GroupCall;
 | 
			
		||||
| 
						 | 
				
			
			@ -87,10 +97,11 @@ interface Props {
 | 
			
		|||
  onLeave: () => void;
 | 
			
		||||
  unencryptedEventsFromUsers: Set<string>;
 | 
			
		||||
  hideHeader: boolean;
 | 
			
		||||
 | 
			
		||||
  matrixInfo: MatrixInfo;
 | 
			
		||||
  mediaDevices: MediaDevicesState;
 | 
			
		||||
  livekitRoom: Room;
 | 
			
		||||
  userChoices: LocalUserChoices;
 | 
			
		||||
  otelGroupCallMembership: OTelGroupCallMembership;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function InCallView({
 | 
			
		||||
| 
						 | 
				
			
			@ -103,10 +114,11 @@ export function InCallView({
 | 
			
		|||
  matrixInfo,
 | 
			
		||||
  mediaDevices,
 | 
			
		||||
  livekitRoom,
 | 
			
		||||
  userChoices,
 | 
			
		||||
  otelGroupCallMembership,
 | 
			
		||||
}: Props) {
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
  usePreventScroll();
 | 
			
		||||
  const joinRule = useJoinRule(groupCall.room);
 | 
			
		||||
 | 
			
		||||
  const containerRef1 = useRef<HTMLDivElement | null>(null);
 | 
			
		||||
  const [containerRef2, bounds] = useMeasure({ polyfill: ResizeObserver });
 | 
			
		||||
| 
						 | 
				
			
			@ -142,8 +154,8 @@ export function InCallView({
 | 
			
		|||
    token,
 | 
			
		||||
    serverUrl: Config.get().livekit.server_url,
 | 
			
		||||
    room: livekitRoom,
 | 
			
		||||
    audio: true,
 | 
			
		||||
    video: true,
 | 
			
		||||
    audio: !userChoices.audioMuted,
 | 
			
		||||
    video: !userChoices.videoMuted,
 | 
			
		||||
    onConnected: () => {
 | 
			
		||||
      console.log("connected to LiveKit room");
 | 
			
		||||
    },
 | 
			
		||||
| 
						 | 
				
			
			@ -167,9 +179,6 @@ export function InCallView({
 | 
			
		|||
 | 
			
		||||
  const [showInspector] = useShowInspector();
 | 
			
		||||
 | 
			
		||||
  const { modalState: feedbackModalState, modalProps: feedbackModalProps } =
 | 
			
		||||
    useModalTriggerState();
 | 
			
		||||
 | 
			
		||||
  const { hideScreensharing } = useUrlParams();
 | 
			
		||||
 | 
			
		||||
  const {
 | 
			
		||||
| 
						 | 
				
			
			@ -185,12 +194,14 @@ export function InCallView({
 | 
			
		|||
  const toggleCamera = useCallback(async () => {
 | 
			
		||||
    await localParticipant.setCameraEnabled(!isCameraEnabled);
 | 
			
		||||
  }, [localParticipant, isCameraEnabled]);
 | 
			
		||||
  const toggleScreenSharing = useCallback(async () => {
 | 
			
		||||
  const toggleScreensharing = useCallback(async () => {
 | 
			
		||||
    await localParticipant.setScreenShareEnabled(!isScreenShareEnabled);
 | 
			
		||||
  }, [localParticipant, isScreenShareEnabled]);
 | 
			
		||||
 | 
			
		||||
  const joinRule = useJoinRule(groupCall.room);
 | 
			
		||||
 | 
			
		||||
  useCallViewKeyboardShortcuts(
 | 
			
		||||
    !feedbackModalState.isOpen,
 | 
			
		||||
    containerRef1,
 | 
			
		||||
    toggleMicrophone,
 | 
			
		||||
    toggleCamera,
 | 
			
		||||
    async (muted) => await localParticipant.setMicrophoneEnabled(!muted)
 | 
			
		||||
| 
						 | 
				
			
			@ -235,10 +246,22 @@ export function InCallView({
 | 
			
		|||
  const reducedControls = boundsValid && bounds.width <= 400;
 | 
			
		||||
  const noControls = reducedControls && bounds.height <= 400;
 | 
			
		||||
 | 
			
		||||
  const prefersReducedMotion = usePrefersReducedMotion();
 | 
			
		||||
 | 
			
		||||
  const items = useParticipantTiles(livekitRoom, participants);
 | 
			
		||||
 | 
			
		||||
  // The maximised participant is the focused (active) participant, given the
 | 
			
		||||
  // window is too small to show everyone
 | 
			
		||||
  const maximisedParticipant = useMemo(
 | 
			
		||||
    () =>
 | 
			
		||||
      noControls
 | 
			
		||||
        ? items.find((item) => item.focused) ?? items.at(0) ?? null
 | 
			
		||||
        : null,
 | 
			
		||||
    [noControls, items]
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const [newGrid] = useNewGrid();
 | 
			
		||||
  const Grid = newGrid ? NewVideoGrid : VideoGrid;
 | 
			
		||||
  const prefersReducedMotion = usePrefersReducedMotion();
 | 
			
		||||
 | 
			
		||||
  const renderContent = (): JSX.Element => {
 | 
			
		||||
    if (items.length === 0) {
 | 
			
		||||
      return (
 | 
			
		||||
| 
						 | 
				
			
			@ -247,15 +270,26 @@ export function InCallView({
 | 
			
		|||
        </div>
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    if (maximisedParticipant) {
 | 
			
		||||
      return (
 | 
			
		||||
        <VideoTileContainer
 | 
			
		||||
          targetHeight={bounds.height}
 | 
			
		||||
          targetWidth={bounds.width}
 | 
			
		||||
          id={maximisedParticipant.id}
 | 
			
		||||
          key={maximisedParticipant.id}
 | 
			
		||||
          item={maximisedParticipant.data}
 | 
			
		||||
        />
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <VideoGrid
 | 
			
		||||
      <Grid
 | 
			
		||||
        items={items}
 | 
			
		||||
        layout={layout}
 | 
			
		||||
        disableAnimations={prefersReducedMotion || isSafari}
 | 
			
		||||
      >
 | 
			
		||||
        {(child) => <VideoTileContainer item={child.data} {...child} />}
 | 
			
		||||
      </VideoGrid>
 | 
			
		||||
      </Grid>
 | 
			
		||||
    );
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -264,6 +298,36 @@ export function InCallView({
 | 
			
		|||
    modalProps: rageshakeRequestModalProps,
 | 
			
		||||
  } = useRageshakeRequestModal(groupCall.room.roomId);
 | 
			
		||||
 | 
			
		||||
  const {
 | 
			
		||||
    modalState: settingsModalState,
 | 
			
		||||
    modalProps: settingsModalProps,
 | 
			
		||||
  }: {
 | 
			
		||||
    modalState: OverlayTriggerState;
 | 
			
		||||
    modalProps: {
 | 
			
		||||
      isOpen: boolean;
 | 
			
		||||
      onClose: () => void;
 | 
			
		||||
    };
 | 
			
		||||
  } = useModalTriggerState();
 | 
			
		||||
 | 
			
		||||
  const openSettings = useCallback(() => {
 | 
			
		||||
    settingsModalState.open();
 | 
			
		||||
  }, [settingsModalState]);
 | 
			
		||||
 | 
			
		||||
  const {
 | 
			
		||||
    modalState: inviteModalState,
 | 
			
		||||
    modalProps: inviteModalProps,
 | 
			
		||||
  }: {
 | 
			
		||||
    modalState: OverlayTriggerState;
 | 
			
		||||
    modalProps: {
 | 
			
		||||
      isOpen: boolean;
 | 
			
		||||
      onClose: () => void;
 | 
			
		||||
    };
 | 
			
		||||
  } = useModalTriggerState();
 | 
			
		||||
 | 
			
		||||
  const openInvite = useCallback(() => {
 | 
			
		||||
    inviteModalState.open();
 | 
			
		||||
  }, [inviteModalState]);
 | 
			
		||||
 | 
			
		||||
  const containerClasses = classNames(styles.inRoom, {
 | 
			
		||||
    [styles.maximised]: undefined,
 | 
			
		||||
  });
 | 
			
		||||
| 
						 | 
				
			
			@ -272,36 +336,44 @@ export function InCallView({
 | 
			
		|||
 | 
			
		||||
  if (noControls) {
 | 
			
		||||
    footer = null;
 | 
			
		||||
  } else if (reducedControls) {
 | 
			
		||||
    footer = (
 | 
			
		||||
      <div className={styles.footer}>
 | 
			
		||||
        <MicButton muted={!isMicrophoneEnabled} onPress={toggleMicrophone} />
 | 
			
		||||
        <VideoButton muted={!isCameraEnabled} onPress={toggleCamera} />
 | 
			
		||||
        <HangupButton onPress={onLeave} />
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  } else {
 | 
			
		||||
    footer = (
 | 
			
		||||
      <div className={styles.footer}>
 | 
			
		||||
        <MicButton muted={!isMicrophoneEnabled} onPress={toggleMicrophone} />
 | 
			
		||||
        <VideoButton muted={!isCameraEnabled} onPress={toggleCamera} />
 | 
			
		||||
        {canScreenshare && !hideScreensharing && !isSafari && (
 | 
			
		||||
          <ScreenshareButton
 | 
			
		||||
            enabled={isScreenShareEnabled}
 | 
			
		||||
            onPress={toggleScreenSharing}
 | 
			
		||||
          />
 | 
			
		||||
        )}
 | 
			
		||||
        <OverflowMenu
 | 
			
		||||
          roomId={matrixInfo.roomId}
 | 
			
		||||
          mediaDevices={mediaDevices}
 | 
			
		||||
          inCall
 | 
			
		||||
          showInvite={joinRule === JoinRule.Public}
 | 
			
		||||
          feedbackModalState={feedbackModalState}
 | 
			
		||||
          feedbackModalProps={feedbackModalProps}
 | 
			
		||||
        />
 | 
			
		||||
        <HangupButton onPress={onLeave} />
 | 
			
		||||
      </div>
 | 
			
		||||
    const buttons: JSX.Element[] = [];
 | 
			
		||||
 | 
			
		||||
    buttons.push(
 | 
			
		||||
      <MicButton
 | 
			
		||||
        key="1"
 | 
			
		||||
        muted={!isMicrophoneEnabled}
 | 
			
		||||
        onPress={toggleMicrophone}
 | 
			
		||||
        data-testid="incall_mute"
 | 
			
		||||
      />,
 | 
			
		||||
      <VideoButton
 | 
			
		||||
        key="2"
 | 
			
		||||
        muted={!isCameraEnabled}
 | 
			
		||||
        onPress={toggleCamera}
 | 
			
		||||
        data-testid="incall_videomute"
 | 
			
		||||
      />
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    if (!reducedControls) {
 | 
			
		||||
      if (canScreenshare && !hideScreensharing && !isSafari) {
 | 
			
		||||
        buttons.push(
 | 
			
		||||
          <ScreenshareButton
 | 
			
		||||
            key="3"
 | 
			
		||||
            enabled={isScreenShareEnabled}
 | 
			
		||||
            onPress={toggleScreensharing}
 | 
			
		||||
            data-testid="incall_screenshare"
 | 
			
		||||
          />
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
      if (!maximisedParticipant) {
 | 
			
		||||
        buttons.push(<SettingsButton key="4" onPress={openSettings} />);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    buttons.push(
 | 
			
		||||
      <HangupButton key="6" onPress={onLeave} data-testid="incall_leave" />
 | 
			
		||||
    );
 | 
			
		||||
    footer = <div className={styles.footer}>{buttons}</div>;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
| 
						 | 
				
			
			@ -320,21 +392,40 @@ export function InCallView({
 | 
			
		|||
          </LeftNav>
 | 
			
		||||
          <RightNav>
 | 
			
		||||
            <GridLayoutMenu layout={layout} setLayout={setLayout} />
 | 
			
		||||
            <UserMenuContainer preventNavigation />
 | 
			
		||||
            {joinRule === JoinRule.Public && (
 | 
			
		||||
              <InviteButton variant="icon" onClick={openInvite} />
 | 
			
		||||
            )}
 | 
			
		||||
          </RightNav>
 | 
			
		||||
        </Header>
 | 
			
		||||
      )}
 | 
			
		||||
      {renderContent()}
 | 
			
		||||
      {footer}
 | 
			
		||||
      <div className={styles.controlsOverlay}>
 | 
			
		||||
        {renderContent()}
 | 
			
		||||
        {footer}
 | 
			
		||||
      </div>
 | 
			
		||||
      <GroupCallInspector
 | 
			
		||||
        client={client}
 | 
			
		||||
        groupCall={groupCall}
 | 
			
		||||
        otelGroupCallMembership={otelGroupCallMembership}
 | 
			
		||||
        show={showInspector}
 | 
			
		||||
      />
 | 
			
		||||
      {rageshakeRequestModalState.isOpen && (
 | 
			
		||||
      {rageshakeRequestModalState.isOpen && !noControls && (
 | 
			
		||||
        <RageshakeRequestModal
 | 
			
		||||
          {...rageshakeRequestModalProps}
 | 
			
		||||
          roomIdOrAlias={matrixInfo.roomId}
 | 
			
		||||
          roomIdOrAlias={matrixInfo.roomIdOrAlias}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
      {settingsModalState.isOpen && (
 | 
			
		||||
        <SettingsModal
 | 
			
		||||
          client={client}
 | 
			
		||||
          roomId={groupCall.room.roomId}
 | 
			
		||||
          mediaDevices={mediaDevices}
 | 
			
		||||
          {...settingsModalProps}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
      {inviteModalState.isOpen && (
 | 
			
		||||
        <InviteModal
 | 
			
		||||
          roomIdOrAlias={matrixInfo.roomIdOrAlias}
 | 
			
		||||
          {...inviteModalProps}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
    </div>
 | 
			
		||||
| 
						 | 
				
			
			@ -371,6 +462,7 @@ function useParticipantTiles(
 | 
			
		|||
          focused: false,
 | 
			
		||||
          local: sfuParticipant.isLocal,
 | 
			
		||||
          data: {
 | 
			
		||||
            id,
 | 
			
		||||
            member,
 | 
			
		||||
            sfuParticipant,
 | 
			
		||||
            content: TileContent.UserMedia,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue