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