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:
Šimon Brandner 2022-08-12 20:54:04 +02:00
commit e82ed2cbcb
No known key found for this signature in database
GPG key ID: D1D45825D60C24D2
11 changed files with 338 additions and 171 deletions

View file

@ -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;

View file

@ -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,7 +157,8 @@ export function InCallView({
return participants;
}, [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 size = Math.round(Math.min(width, height) / 2);
@ -170,7 +171,9 @@ export function InCallView({
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 {

View file

@ -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,

View file

@ -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}
/>

View file

@ -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) {

View file

@ -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>}
{audioMuted && !videoMuted && <MicMutedIcon />}
{videoMuted && <VideoMutedIcon />}
<span title={name}>{name}</span>
</div>
)
)}
<video ref={mediaRef} playsInline disablePictureInPicture />
</animated.div>
);

View file

@ -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)}

View file

@ -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);
}

View file

@ -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 }));
}

View file

@ -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",

View file

@ -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]);