mirror of
https://github.com/vector-im/element-call.git
synced 2024-11-24 00:38:31 +08:00
Update to use matrix-react-sdk
This commit is contained in:
parent
c4a626b530
commit
5e4736eba5
26
README.md
26
README.md
@ -6,32 +6,8 @@ Testbed for full mesh video chat.
|
|||||||
|
|
||||||
You must first run a local Synapse server on port 8008
|
You must first run a local Synapse server on port 8008
|
||||||
|
|
||||||
Then install the dependencies:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
cd matrix-video-chat
|
cd matrix-video-chat
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
Locally checkout the [robertlong/full-mesh-voip](https://github.com/matrix-org/matrix-js-sdk/tree/robertlong/full-mesh-voip) branch of the matrix-js-sdk.
|
|
||||||
|
|
||||||
```
|
|
||||||
cd matrix-js-sdk
|
|
||||||
git checkout robertlong/full-mesh-voip
|
|
||||||
yarn
|
yarn
|
||||||
yarn build
|
yarn dev
|
||||||
npm link
|
|
||||||
```
|
|
||||||
|
|
||||||
Link the matrix-js-sdk into the matrix-video-chat project:
|
|
||||||
|
|
||||||
```
|
|
||||||
cd matrix-video-chat
|
|
||||||
npm link matrix-js-sdk
|
|
||||||
```
|
|
||||||
|
|
||||||
Finally run the development server
|
|
||||||
|
|
||||||
```
|
|
||||||
npm run dev
|
|
||||||
```
|
```
|
3064
package-lock.json
generated
3064
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
11
package.json
11
package.json
@ -11,19 +11,18 @@
|
|||||||
"color-hash": "^2.0.1",
|
"color-hash": "^2.0.1",
|
||||||
"events": "^3.3.0",
|
"events": "^3.3.0",
|
||||||
"lodash-move": "^1.1.1",
|
"lodash-move": "^1.1.1",
|
||||||
"matrix-js-sdk": "file:../matrix-js-sdk",
|
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#robertlong/group-call",
|
||||||
"matrix-react-sdk": "file:../matrix-react-sdk",
|
"matrix-react-sdk": "github:matrix-org/matrix-react-sdk#robertlong/group-call",
|
||||||
"re-resizable": "^6.9.0",
|
"re-resizable": "^6.9.0",
|
||||||
"react": "^17.0.0",
|
"react": "^17.0.0",
|
||||||
"react-dom": "^17.0.0",
|
"react-dom": "^17.0.0",
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
"react-use-gesture": "^9.1.3",
|
"react-use-gesture": "^9.1.3",
|
||||||
"react-use-measure": "^2.0.4",
|
"react-use-measure": "^2.0.4"
|
||||||
"sass": "^1.42.1",
|
|
||||||
"vite-plugin-svgr": "^0.4.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-react-refresh": "^1.3.1",
|
"vite-plugin-svgr": "^0.4.0",
|
||||||
|
"sass": "^1.42.1",
|
||||||
"vite": "^2.4.2"
|
"vite": "^2.4.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,6 @@ import {
|
|||||||
import { useClient } from "./ConferenceCallManagerHooks";
|
import { useClient } from "./ConferenceCallManagerHooks";
|
||||||
import { Home } from "./Home";
|
import { Home } from "./Home";
|
||||||
import { Room } from "./Room";
|
import { Room } from "./Room";
|
||||||
import { GridDemo } from "./GridDemo";
|
|
||||||
import { RegisterPage } from "./RegisterPage";
|
import { RegisterPage } from "./RegisterPage";
|
||||||
import { LoginPage } from "./LoginPage";
|
import { LoginPage } from "./LoginPage";
|
||||||
import { Center } from "./Layout";
|
import { Center } from "./Layout";
|
||||||
@ -70,9 +69,6 @@ export default function App() {
|
|||||||
<GuestAuthPage onRegisterGuest={registerGuest} />
|
<GuestAuthPage onRegisterGuest={registerGuest} />
|
||||||
)}
|
)}
|
||||||
</Route>
|
</Route>
|
||||||
<Route exact path="/grid">
|
|
||||||
<GridDemo />
|
|
||||||
</Route>
|
|
||||||
</Switch>
|
</Switch>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
@ -14,25 +14,8 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import matrix from "matrix-js-sdk/src/index";
|
import matrix from "matrix-js-sdk/src/browser-index";
|
||||||
import { ConferenceCallDebugger } from "./ConferenceCallDebugger";
|
|
||||||
|
|
||||||
// https://stackoverflow.com/a/9039885
|
|
||||||
function isIOS() {
|
|
||||||
return (
|
|
||||||
[
|
|
||||||
"iPad Simulator",
|
|
||||||
"iPhone Simulator",
|
|
||||||
"iPod Simulator",
|
|
||||||
"iPad",
|
|
||||||
"iPhone",
|
|
||||||
"iPod",
|
|
||||||
].includes(navigator.platform) ||
|
|
||||||
// iPad on iOS 13 detection
|
|
||||||
(navigator.userAgent.includes("Mac") && "ontouchend" in document)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function waitForSync(client) {
|
function waitForSync(client) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@ -245,210 +228,6 @@ export function useClient(homeserverUrl) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function usePageUnload(callback) {
|
|
||||||
useEffect(() => {
|
|
||||||
let pageVisibilityTimeout;
|
|
||||||
|
|
||||||
function onBeforeUnload(event) {
|
|
||||||
if (event.type === "visibilitychange") {
|
|
||||||
if (document.visibilityState === "visible") {
|
|
||||||
clearTimeout(pageVisibilityTimeout);
|
|
||||||
} else {
|
|
||||||
// Wait 5 seconds before closing the page to avoid accidentally leaving
|
|
||||||
// TODO: Make this configurable?
|
|
||||||
pageVisibilityTimeout = setTimeout(() => {
|
|
||||||
callback();
|
|
||||||
}, 5000);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// iOS doesn't fire beforeunload event, so leave the call when you hide the page.
|
|
||||||
if (isIOS()) {
|
|
||||||
window.addEventListener("pagehide", onBeforeUnload);
|
|
||||||
document.addEventListener("visibilitychange", onBeforeUnload);
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener("beforeunload", onBeforeUnload);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener("pagehide", onBeforeUnload);
|
|
||||||
document.removeEventListener("visibilitychange", onBeforeUnload);
|
|
||||||
window.removeEventListener("beforeunload", onBeforeUnload);
|
|
||||||
clearTimeout(pageVisibilityTimeout);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getParticipants(groupCall) {
|
|
||||||
return [...groupCall.participants];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useGroupCall(client, roomId, debug = false) {
|
|
||||||
const groupCallRef = useRef(null);
|
|
||||||
|
|
||||||
const [
|
|
||||||
{
|
|
||||||
loading,
|
|
||||||
entered,
|
|
||||||
entering,
|
|
||||||
room,
|
|
||||||
participants,
|
|
||||||
error,
|
|
||||||
microphoneMuted,
|
|
||||||
localVideoMuted,
|
|
||||||
callDebugger,
|
|
||||||
},
|
|
||||||
setState,
|
|
||||||
] = useState({
|
|
||||||
loading: true,
|
|
||||||
entered: false,
|
|
||||||
entering: false,
|
|
||||||
room: null,
|
|
||||||
participants: [],
|
|
||||||
error: null,
|
|
||||||
microphoneMuted: false,
|
|
||||||
localVideoMuted: false,
|
|
||||||
callDebugger: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
const updateState = (state) =>
|
|
||||||
setState((prevState) => ({ ...prevState, ...state }));
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
function onParticipantsChanged(...args) {
|
|
||||||
updateState({ participants: getParticipants(groupCallRef.current) });
|
|
||||||
}
|
|
||||||
|
|
||||||
function onLocalMuteStateChanged(microphoneMuted, localVideoMuted) {
|
|
||||||
updateState({
|
|
||||||
microphoneMuted,
|
|
||||||
localVideoMuted,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function init() {
|
|
||||||
const room = await fetchRoom(client, roomId, true);
|
|
||||||
|
|
||||||
const groupCall = client.createGroupCall(roomId, "video");
|
|
||||||
groupCallRef.current = groupCall;
|
|
||||||
groupCall.on("active_speaker_changed", onParticipantsChanged);
|
|
||||||
groupCall.on("participants_changed", onParticipantsChanged);
|
|
||||||
groupCall.on("speaking", onParticipantsChanged);
|
|
||||||
groupCall.on("mute_state_changed", onParticipantsChanged);
|
|
||||||
groupCall.on("call_replaced", onParticipantsChanged);
|
|
||||||
groupCall.on("call_feeds_changed", onParticipantsChanged);
|
|
||||||
groupCall.on("local_mute_state_changed", onLocalMuteStateChanged);
|
|
||||||
|
|
||||||
updateState({
|
|
||||||
room,
|
|
||||||
loading: false,
|
|
||||||
callDebugger: debug
|
|
||||||
? new ConferenceCallDebugger(client, groupCall)
|
|
||||||
: null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
init().catch((error) => {
|
|
||||||
if (groupCallRef.current) {
|
|
||||||
const groupCall = groupCallRef.current;
|
|
||||||
groupCall.removeListener(
|
|
||||||
"active_speaker_changed",
|
|
||||||
onParticipantsChanged
|
|
||||||
);
|
|
||||||
groupCall.removeListener("participants_changed", onParticipantsChanged);
|
|
||||||
groupCall.removeListener("speaking", onParticipantsChanged);
|
|
||||||
groupCall.removeListener("mute_state_changed", onParticipantsChanged);
|
|
||||||
groupCall.removeListener("call_replaced", onParticipantsChanged);
|
|
||||||
groupCall.removeListener("call_feeds_changed", onParticipantsChanged);
|
|
||||||
groupCall.removeListener(
|
|
||||||
"local_mute_state_changed",
|
|
||||||
onLocalMuteStateChanged
|
|
||||||
);
|
|
||||||
groupCall.leave();
|
|
||||||
}
|
|
||||||
|
|
||||||
updateState({ error, loading: false });
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (groupCallRef.current) {
|
|
||||||
groupCallRef.current.leave();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [client, roomId]);
|
|
||||||
|
|
||||||
usePageUnload(() => {
|
|
||||||
if (groupCallRef.current) {
|
|
||||||
groupCallRef.current.leave();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const initLocalParticipant = useCallback(
|
|
||||||
() => groupCallRef.current.initLocalParticipant(),
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const enter = useCallback(() => {
|
|
||||||
updateState({ entering: true });
|
|
||||||
|
|
||||||
groupCallRef.current
|
|
||||||
.enter()
|
|
||||||
.then(() => {
|
|
||||||
updateState({
|
|
||||||
entered: true,
|
|
||||||
entering: false,
|
|
||||||
participants: getParticipants(groupCallRef.current),
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
updateState({ error, entering: false });
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const leave = useCallback(() => {
|
|
||||||
groupCallRef.current.leave();
|
|
||||||
updateState({
|
|
||||||
entered: false,
|
|
||||||
participants: [],
|
|
||||||
microphoneMuted: false,
|
|
||||||
localVideoMuted: false,
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const toggleLocalVideoMuted = useCallback(() => {
|
|
||||||
groupCallRef.current.setLocalVideoMuted(
|
|
||||||
!groupCallRef.current.isLocalVideoMuted()
|
|
||||||
);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const toggleMicrophoneMuted = useCallback(() => {
|
|
||||||
groupCallRef.current.setMicrophoneMuted(
|
|
||||||
!groupCallRef.current.isMicrophoneMuted()
|
|
||||||
);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return {
|
|
||||||
loading,
|
|
||||||
entered,
|
|
||||||
entering,
|
|
||||||
roomName: room ? room.name : null,
|
|
||||||
participants,
|
|
||||||
groupCall: groupCallRef.current,
|
|
||||||
callDebugger: callDebugger,
|
|
||||||
microphoneMuted,
|
|
||||||
localVideoMuted,
|
|
||||||
error,
|
|
||||||
initLocalParticipant,
|
|
||||||
enter,
|
|
||||||
leave,
|
|
||||||
toggleLocalVideoMuted,
|
|
||||||
toggleMicrophoneMuted,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const tsCache = {};
|
const tsCache = {};
|
||||||
|
|
||||||
function getLastTs(client, r) {
|
function getLastTs(client, r) {
|
||||||
|
@ -1,49 +0,0 @@
|
|||||||
import React, { useCallback, useRef, useState } from "react";
|
|
||||||
import styles from "./GridDemo.module.css";
|
|
||||||
import { VideoGrid } from "./VideoGrid";
|
|
||||||
|
|
||||||
export function GridDemo() {
|
|
||||||
const participantKey = useRef(0);
|
|
||||||
const [stream, setStream] = useState();
|
|
||||||
const [participants, setParticipants] = useState([]);
|
|
||||||
|
|
||||||
const startWebcam = useCallback(async () => {
|
|
||||||
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
|
|
||||||
setStream(stream);
|
|
||||||
setParticipants([{ stream, userId: participantKey.current++ }]);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const addParticipant = useCallback(() => {
|
|
||||||
setParticipants((participants) => [
|
|
||||||
...participants,
|
|
||||||
{ stream: stream.clone(), userId: participantKey.current++ },
|
|
||||||
]);
|
|
||||||
}, [stream]);
|
|
||||||
|
|
||||||
const removeParticipant = useCallback((key) => {
|
|
||||||
setParticipants((participants) =>
|
|
||||||
participants.filter((participant) => participant.userId !== key)
|
|
||||||
);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.gridDemo}>
|
|
||||||
<div className={styles.buttons}>
|
|
||||||
{!stream && <button onClick={startWebcam}>Start Webcam</button>}
|
|
||||||
{stream && participants.length < 12 && (
|
|
||||||
<button onClick={addParticipant}>Add Tile</button>
|
|
||||||
)}
|
|
||||||
{stream && participants.length > 0 && (
|
|
||||||
<button
|
|
||||||
onClick={() =>
|
|
||||||
removeParticipant(participants[participants.length - 1].userId)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Remove Tile
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<VideoGrid participants={participants} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
.gridDemo {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
@ -30,7 +30,9 @@ import {
|
|||||||
GroupCallState,
|
GroupCallState,
|
||||||
GroupCallType,
|
GroupCallType,
|
||||||
} from "matrix-js-sdk/src/webrtc/groupCall";
|
} from "matrix-js-sdk/src/webrtc/groupCall";
|
||||||
import VideoGrid from "matrix-react-sdk/src/components/views/voip/GroupCallView/VideoGrid";
|
import VideoGrid, {
|
||||||
|
useVideoGridLayout,
|
||||||
|
} from "matrix-react-sdk/src/components/views/voip/GroupCallView/VideoGrid";
|
||||||
import "matrix-react-sdk/res/css/views/voip/GroupCallView/_VideoGrid.scss";
|
import "matrix-react-sdk/res/css/views/voip/GroupCallView/_VideoGrid.scss";
|
||||||
import { useGroupCall } from "matrix-react-sdk/src/hooks/useGroupCall";
|
import { useGroupCall } from "matrix-react-sdk/src/hooks/useGroupCall";
|
||||||
import { useCallFeed } from "matrix-react-sdk/src/hooks/useCallFeed";
|
import { useCallFeed } from "matrix-react-sdk/src/hooks/useCallFeed";
|
||||||
|
@ -1,763 +0,0 @@
|
|||||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
|
||||||
import { useDrag } from "react-use-gesture";
|
|
||||||
import { useSprings, animated } from "@react-spring/web";
|
|
||||||
import classNames from "classnames";
|
|
||||||
import styles from "./VideoGrid.module.css";
|
|
||||||
import useMeasure from "react-use-measure";
|
|
||||||
import moveArrItem from "lodash-move";
|
|
||||||
import { ReactComponent as MicIcon } from "./icons/Mic.svg";
|
|
||||||
import { ReactComponent as MuteMicIcon } from "./icons/MuteMic.svg";
|
|
||||||
import { ReactComponent as DisableVideoIcon } from "./icons/DisableVideo.svg";
|
|
||||||
|
|
||||||
function useIsMounted() {
|
|
||||||
const isMountedRef = useRef(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
isMountedRef.current = true;
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
isMountedRef.current = false;
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return isMountedRef;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isInside([x, y], targetTile) {
|
|
||||||
const left = targetTile.x;
|
|
||||||
const top = targetTile.y;
|
|
||||||
const bottom = targetTile.y + targetTile.height;
|
|
||||||
const right = targetTile.x + targetTile.width;
|
|
||||||
|
|
||||||
if (x < left || x > right || y < top || y > bottom) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTilePositions(
|
|
||||||
tileCount,
|
|
||||||
presenterTileCount,
|
|
||||||
gridWidth,
|
|
||||||
gridHeight
|
|
||||||
) {
|
|
||||||
if (tileCount === 0) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tileCount > 12) {
|
|
||||||
console.warn("Over 12 tiles is not currently supported");
|
|
||||||
}
|
|
||||||
|
|
||||||
const gap = 8;
|
|
||||||
|
|
||||||
const { layoutDirection, participantGridRatio } = getGridLayout(
|
|
||||||
tileCount,
|
|
||||||
presenterTileCount,
|
|
||||||
gridWidth,
|
|
||||||
gridHeight
|
|
||||||
);
|
|
||||||
|
|
||||||
let participantGridWidth, participantGridHeight;
|
|
||||||
|
|
||||||
if (layoutDirection === "vertical") {
|
|
||||||
participantGridWidth = gridWidth;
|
|
||||||
participantGridHeight = Math.round(gridHeight * participantGridRatio);
|
|
||||||
} else {
|
|
||||||
participantGridWidth = Math.round(gridWidth * participantGridRatio);
|
|
||||||
participantGridHeight = gridHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
const participantTileCount = tileCount - presenterTileCount;
|
|
||||||
|
|
||||||
const participantGridPositions = getSubGridPositions(
|
|
||||||
participantTileCount,
|
|
||||||
participantGridWidth,
|
|
||||||
participantGridHeight,
|
|
||||||
gap
|
|
||||||
);
|
|
||||||
const participantGridBounds = getSubGridBoundingBox(participantGridPositions);
|
|
||||||
|
|
||||||
let presenterGridWidth, presenterGridHeight;
|
|
||||||
|
|
||||||
if (presenterTileCount === 0) {
|
|
||||||
presenterGridWidth = 0;
|
|
||||||
presenterGridHeight = 0;
|
|
||||||
} else if (layoutDirection === "vertical") {
|
|
||||||
presenterGridWidth = gridWidth;
|
|
||||||
presenterGridHeight =
|
|
||||||
gridHeight -
|
|
||||||
(participantGridBounds.height + (participantTileCount ? gap * 2 : 0));
|
|
||||||
} else {
|
|
||||||
presenterGridWidth =
|
|
||||||
gridWidth -
|
|
||||||
(participantGridBounds.width + (participantTileCount ? gap * 2 : 0));
|
|
||||||
presenterGridHeight = gridHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
const presenterGridPositions = getSubGridPositions(
|
|
||||||
presenterTileCount,
|
|
||||||
presenterGridWidth,
|
|
||||||
presenterGridHeight,
|
|
||||||
gap
|
|
||||||
);
|
|
||||||
|
|
||||||
const tilePositions = [
|
|
||||||
...presenterGridPositions,
|
|
||||||
...participantGridPositions,
|
|
||||||
];
|
|
||||||
|
|
||||||
centerTiles(
|
|
||||||
presenterGridPositions,
|
|
||||||
presenterGridWidth,
|
|
||||||
presenterGridHeight,
|
|
||||||
0,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
|
|
||||||
if (layoutDirection === "vertical") {
|
|
||||||
centerTiles(
|
|
||||||
participantGridPositions,
|
|
||||||
gridWidth,
|
|
||||||
gridHeight - presenterGridHeight,
|
|
||||||
0,
|
|
||||||
presenterGridHeight
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
centerTiles(
|
|
||||||
participantGridPositions,
|
|
||||||
gridWidth - presenterGridWidth,
|
|
||||||
gridHeight,
|
|
||||||
presenterGridWidth,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return tilePositions;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSubGridBoundingBox(positions) {
|
|
||||||
let left = 0,
|
|
||||||
right = 0,
|
|
||||||
top = 0,
|
|
||||||
bottom = 0;
|
|
||||||
|
|
||||||
for (let i = 0; i < positions.length; i++) {
|
|
||||||
const { x, y, width, height } = positions[i];
|
|
||||||
|
|
||||||
if (i === 0) {
|
|
||||||
left = x;
|
|
||||||
right = x + width;
|
|
||||||
top = y;
|
|
||||||
bottom = y + height;
|
|
||||||
} else {
|
|
||||||
if (x < left) {
|
|
||||||
left = x;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (y < top) {
|
|
||||||
top = y;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (x + width > right) {
|
|
||||||
right = x + width;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (y + height > bottom) {
|
|
||||||
bottom = y + height;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
left,
|
|
||||||
right,
|
|
||||||
top,
|
|
||||||
bottom,
|
|
||||||
width: right - left,
|
|
||||||
height: bottom - top,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function getGridLayout(tileCount, presenterTileCount, gridWidth, gridHeight) {
|
|
||||||
let layoutDirection = "horizontal";
|
|
||||||
let participantGridRatio = 1;
|
|
||||||
|
|
||||||
if (presenterTileCount === 0) {
|
|
||||||
return { participantGridRatio, layoutDirection };
|
|
||||||
}
|
|
||||||
|
|
||||||
const gridAspectRatio = gridWidth / gridHeight;
|
|
||||||
|
|
||||||
if (gridAspectRatio < 1) {
|
|
||||||
layoutDirection = "vertical";
|
|
||||||
participantGridRatio = 1 / 3;
|
|
||||||
} else {
|
|
||||||
layoutDirection = "horizontal";
|
|
||||||
participantGridRatio = 1 / 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { participantGridRatio, layoutDirection };
|
|
||||||
}
|
|
||||||
|
|
||||||
function centerTiles(positions, gridWidth, gridHeight, offsetLeft, offsetTop) {
|
|
||||||
const bounds = getSubGridBoundingBox(positions);
|
|
||||||
|
|
||||||
const leftOffset = Math.round((gridWidth - bounds.width) / 2) + offsetLeft;
|
|
||||||
const topOffset = Math.round((gridHeight - bounds.height) / 2) + offsetTop;
|
|
||||||
|
|
||||||
applyTileOffsets(positions, leftOffset, topOffset);
|
|
||||||
|
|
||||||
return positions;
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyTileOffsets(positions, leftOffset, topOffset) {
|
|
||||||
for (const position of positions) {
|
|
||||||
position.x += leftOffset;
|
|
||||||
position.y += topOffset;
|
|
||||||
}
|
|
||||||
|
|
||||||
return positions;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSubGridLayout(tileCount, gridWidth, gridHeight) {
|
|
||||||
const gridAspectRatio = gridWidth / gridHeight;
|
|
||||||
|
|
||||||
let columnCount, rowCount;
|
|
||||||
let tileAspectRatio = 16 / 9;
|
|
||||||
|
|
||||||
if (gridAspectRatio < 3 / 4) {
|
|
||||||
// Phone
|
|
||||||
if (tileCount === 1) {
|
|
||||||
columnCount = 1;
|
|
||||||
rowCount = 1;
|
|
||||||
tileAspectRatio = 0;
|
|
||||||
} else if (tileCount <= 4) {
|
|
||||||
columnCount = 1;
|
|
||||||
rowCount = tileCount;
|
|
||||||
} else if (tileCount <= 12) {
|
|
||||||
columnCount = 2;
|
|
||||||
rowCount = Math.ceil(tileCount / columnCount);
|
|
||||||
tileAspectRatio = 0;
|
|
||||||
} else {
|
|
||||||
// Unsupported
|
|
||||||
columnCount = 3;
|
|
||||||
rowCount = Math.ceil(tileCount / columnCount);
|
|
||||||
tileAspectRatio = 1;
|
|
||||||
}
|
|
||||||
} else if (gridAspectRatio < 1) {
|
|
||||||
// Tablet
|
|
||||||
if (tileCount === 1) {
|
|
||||||
columnCount = 1;
|
|
||||||
rowCount = 1;
|
|
||||||
tileAspectRatio = 0;
|
|
||||||
} else if (tileCount <= 4) {
|
|
||||||
columnCount = 1;
|
|
||||||
rowCount = tileCount;
|
|
||||||
} else if (tileCount <= 12) {
|
|
||||||
columnCount = 2;
|
|
||||||
rowCount = Math.ceil(tileCount / columnCount);
|
|
||||||
} else {
|
|
||||||
// Unsupported
|
|
||||||
columnCount = 3;
|
|
||||||
rowCount = Math.ceil(tileCount / columnCount);
|
|
||||||
tileAspectRatio = 1;
|
|
||||||
}
|
|
||||||
} else if (gridAspectRatio < 17 / 9) {
|
|
||||||
// Computer
|
|
||||||
if (tileCount === 1) {
|
|
||||||
columnCount = 1;
|
|
||||||
rowCount = 1;
|
|
||||||
} else if (tileCount === 2) {
|
|
||||||
columnCount = 2;
|
|
||||||
rowCount = 1;
|
|
||||||
} else if (tileCount <= 4) {
|
|
||||||
columnCount = 2;
|
|
||||||
rowCount = 2;
|
|
||||||
} else if (tileCount <= 6) {
|
|
||||||
columnCount = 3;
|
|
||||||
rowCount = 2;
|
|
||||||
} else if (tileCount <= 8) {
|
|
||||||
columnCount = 4;
|
|
||||||
rowCount = 2;
|
|
||||||
tileAspectRatio = 1;
|
|
||||||
} else if (tileCount <= 12) {
|
|
||||||
columnCount = 4;
|
|
||||||
rowCount = 3;
|
|
||||||
tileAspectRatio = 1;
|
|
||||||
} else {
|
|
||||||
// Unsupported
|
|
||||||
columnCount = 4;
|
|
||||||
rowCount = 4;
|
|
||||||
}
|
|
||||||
} else if (gridAspectRatio <= 32 / 9) {
|
|
||||||
// Ultrawide
|
|
||||||
if (tileCount === 1) {
|
|
||||||
columnCount = 1;
|
|
||||||
rowCount = 1;
|
|
||||||
} else if (tileCount === 2) {
|
|
||||||
columnCount = 2;
|
|
||||||
rowCount = 1;
|
|
||||||
} else if (tileCount <= 4) {
|
|
||||||
columnCount = 2;
|
|
||||||
rowCount = 2;
|
|
||||||
} else if (tileCount <= 6) {
|
|
||||||
columnCount = 3;
|
|
||||||
rowCount = 2;
|
|
||||||
} else if (tileCount <= 8) {
|
|
||||||
columnCount = 4;
|
|
||||||
rowCount = 2;
|
|
||||||
} else if (tileCount <= 12) {
|
|
||||||
columnCount = 4;
|
|
||||||
rowCount = 3;
|
|
||||||
} else {
|
|
||||||
// Unsupported
|
|
||||||
columnCount = 4;
|
|
||||||
rowCount = 4;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Super Ultrawide
|
|
||||||
if (tileCount <= 6) {
|
|
||||||
columnCount = tileCount;
|
|
||||||
rowCount = 1;
|
|
||||||
} else {
|
|
||||||
columnCount = Math.ceil(tileCount / 2);
|
|
||||||
rowCount = 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { columnCount, rowCount, tileAspectRatio };
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSubGridPositions(tileCount, gridWidth, gridHeight, gap) {
|
|
||||||
if (tileCount === 0) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const { columnCount, rowCount, tileAspectRatio } = getSubGridLayout(
|
|
||||||
tileCount,
|
|
||||||
gridWidth,
|
|
||||||
gridHeight
|
|
||||||
);
|
|
||||||
|
|
||||||
const newTilePositions = [];
|
|
||||||
|
|
||||||
const boxWidth = Math.round(
|
|
||||||
(gridWidth - gap * (columnCount + 1)) / columnCount
|
|
||||||
);
|
|
||||||
const boxHeight = Math.round((gridHeight - gap * (rowCount + 1)) / rowCount);
|
|
||||||
|
|
||||||
let tileWidth, tileHeight;
|
|
||||||
|
|
||||||
if (tileAspectRatio) {
|
|
||||||
const boxAspectRatio = boxWidth / boxHeight;
|
|
||||||
|
|
||||||
if (boxAspectRatio > tileAspectRatio) {
|
|
||||||
tileWidth = boxHeight * tileAspectRatio;
|
|
||||||
tileHeight = boxHeight;
|
|
||||||
} else {
|
|
||||||
tileWidth = boxWidth;
|
|
||||||
tileHeight = boxWidth / tileAspectRatio;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
tileWidth = boxWidth;
|
|
||||||
tileHeight = boxHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < tileCount; i++) {
|
|
||||||
const verticalIndex = Math.floor(i / columnCount);
|
|
||||||
const top = verticalIndex * gap + verticalIndex * tileHeight;
|
|
||||||
|
|
||||||
let rowItemCount;
|
|
||||||
|
|
||||||
if (verticalIndex + 1 === rowCount && tileCount % columnCount !== 0) {
|
|
||||||
rowItemCount = tileCount % columnCount;
|
|
||||||
} else {
|
|
||||||
rowItemCount = columnCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
const horizontalIndex = i % columnCount;
|
|
||||||
|
|
||||||
let centeringPadding = 0;
|
|
||||||
|
|
||||||
if (rowItemCount < columnCount) {
|
|
||||||
const subgridWidth = tileWidth * columnCount + (gap * columnCount - 1);
|
|
||||||
centeringPadding = Math.round(
|
|
||||||
(subgridWidth - (tileWidth * rowItemCount + (gap * rowItemCount - 1))) /
|
|
||||||
2
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const left =
|
|
||||||
centeringPadding + gap * horizontalIndex + tileWidth * horizontalIndex;
|
|
||||||
|
|
||||||
newTilePositions.push({
|
|
||||||
width: tileWidth,
|
|
||||||
height: tileHeight,
|
|
||||||
x: left,
|
|
||||||
y: top,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return newTilePositions;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function VideoGrid({ participants, layout }) {
|
|
||||||
const [{ tiles, tilePositions }, setTileState] = useState({
|
|
||||||
tiles: [],
|
|
||||||
tilePositions: [],
|
|
||||||
});
|
|
||||||
const draggingTileRef = useRef(null);
|
|
||||||
const lastTappedRef = useRef({});
|
|
||||||
const lastLayoutRef = useRef(layout);
|
|
||||||
const isMounted = useIsMounted();
|
|
||||||
|
|
||||||
const [gridRef, gridBounds] = useMeasure();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTileState(({ tiles }) => {
|
|
||||||
const newTiles = [];
|
|
||||||
const removedTileKeys = [];
|
|
||||||
|
|
||||||
for (const tile of tiles) {
|
|
||||||
let participant = participants.find(
|
|
||||||
(participant) => participant.member.userId === tile.key
|
|
||||||
);
|
|
||||||
|
|
||||||
let remove = false;
|
|
||||||
|
|
||||||
if (!participant) {
|
|
||||||
remove = true;
|
|
||||||
participant = tile.participant;
|
|
||||||
removedTileKeys.push(tile.key);
|
|
||||||
}
|
|
||||||
|
|
||||||
let presenter;
|
|
||||||
|
|
||||||
if (layout === "spotlight") {
|
|
||||||
presenter = participant.isActiveSpeaker();
|
|
||||||
} else {
|
|
||||||
presenter = layout === lastLayoutRef.current ? tile.presenter : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
newTiles.push({
|
|
||||||
key: participant.member.userId,
|
|
||||||
participant,
|
|
||||||
remove,
|
|
||||||
presenter,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const participant of participants) {
|
|
||||||
if (newTiles.some(({ key }) => participant.member.userId === key)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Added tiles
|
|
||||||
newTiles.push({
|
|
||||||
key: participant.member.userId,
|
|
||||||
participant,
|
|
||||||
remove: false,
|
|
||||||
presenter: layout === "spotlight" && participant.isActiveSpeaker(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
newTiles.sort((a, b) => (b.presenter ? 1 : 0) - (a.presenter ? 1 : 0));
|
|
||||||
|
|
||||||
if (removedTileKeys.length > 0) {
|
|
||||||
setTimeout(() => {
|
|
||||||
if (!isMounted.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setTileState(({ tiles }) => {
|
|
||||||
const newTiles = tiles.filter(
|
|
||||||
(tile) => !removedTileKeys.includes(tile.key)
|
|
||||||
);
|
|
||||||
|
|
||||||
const presenterTileCount = newTiles.reduce(
|
|
||||||
(count, tile) => count + (tile.presenter ? 1 : 0),
|
|
||||||
0
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
tiles: newTiles,
|
|
||||||
tilePositions: getTilePositions(
|
|
||||||
newTiles.length,
|
|
||||||
presenterTileCount,
|
|
||||||
gridBounds.width,
|
|
||||||
gridBounds.height
|
|
||||||
),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}, 250);
|
|
||||||
}
|
|
||||||
|
|
||||||
const presenterTileCount = newTiles.reduce(
|
|
||||||
(count, tile) => count + (tile.presenter ? 1 : 0),
|
|
||||||
0
|
|
||||||
);
|
|
||||||
|
|
||||||
lastLayoutRef.current = layout;
|
|
||||||
|
|
||||||
return {
|
|
||||||
tiles: newTiles,
|
|
||||||
tilePositions: getTilePositions(
|
|
||||||
newTiles.length,
|
|
||||||
presenterTileCount,
|
|
||||||
gridBounds.width,
|
|
||||||
gridBounds.height
|
|
||||||
),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}, [participants, gridBounds, layout]);
|
|
||||||
|
|
||||||
const animate = useCallback(
|
|
||||||
(tiles) => (tileIndex) => {
|
|
||||||
const tile = tiles[tileIndex];
|
|
||||||
const tilePosition = tilePositions[tileIndex];
|
|
||||||
const draggingTile = draggingTileRef.current;
|
|
||||||
const dragging = draggingTile && tile.key === draggingTile.key;
|
|
||||||
const remove = tile.remove;
|
|
||||||
|
|
||||||
if (dragging) {
|
|
||||||
return {
|
|
||||||
width: tilePosition.width,
|
|
||||||
height: tilePosition.height,
|
|
||||||
x: draggingTile.offsetX + draggingTile.x,
|
|
||||||
y: draggingTile.offsetY + draggingTile.y,
|
|
||||||
scale: 1.1,
|
|
||||||
opacity: 1,
|
|
||||||
zIndex: 1,
|
|
||||||
shadow: 15,
|
|
||||||
immediate: (key) => key === "zIndex" || key === "x" || key === "y",
|
|
||||||
from: {
|
|
||||||
scale: 0,
|
|
||||||
opacity: 0,
|
|
||||||
},
|
|
||||||
reset: false,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
...tilePosition,
|
|
||||||
scale: remove ? 0 : 1,
|
|
||||||
opacity: remove ? 0 : 1,
|
|
||||||
zIndex: 0,
|
|
||||||
shadow: 1,
|
|
||||||
from: {
|
|
||||||
scale: 0,
|
|
||||||
opacity: 0,
|
|
||||||
},
|
|
||||||
reset: false,
|
|
||||||
immediate: (key) => key === "zIndex",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[tiles, tilePositions]
|
|
||||||
);
|
|
||||||
|
|
||||||
const [springs, api] = useSprings(tiles.length, animate(tiles), [
|
|
||||||
tilePositions,
|
|
||||||
tiles,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const onTap = useCallback(
|
|
||||||
(tileKey) => {
|
|
||||||
const lastTapped = lastTappedRef.current[tileKey];
|
|
||||||
|
|
||||||
if (!lastTapped || Date.now() - lastTapped > 500) {
|
|
||||||
lastTappedRef.current[tileKey] = Date.now();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
lastTappedRef.current[tileKey] = 0;
|
|
||||||
|
|
||||||
const tile = tiles.find((tile) => tile.key === tileKey);
|
|
||||||
|
|
||||||
if (!tile) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const participant = tile.participant;
|
|
||||||
|
|
||||||
setTileState((state) => {
|
|
||||||
let presenterTileCount = 0;
|
|
||||||
|
|
||||||
const newTiles = state.tiles.map((tile) => {
|
|
||||||
let newTile = tile;
|
|
||||||
|
|
||||||
if (tile.participant === participant) {
|
|
||||||
newTile = { ...tile, presenter: !tile.presenter };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newTile.presenter) {
|
|
||||||
presenterTileCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return newTile;
|
|
||||||
});
|
|
||||||
|
|
||||||
newTiles.sort((a, b) => (b.presenter ? 1 : 0) - (a.presenter ? 1 : 0));
|
|
||||||
|
|
||||||
presenterTileCount;
|
|
||||||
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
tiles: newTiles,
|
|
||||||
tilePositions: getTilePositions(
|
|
||||||
newTiles.length,
|
|
||||||
presenterTileCount,
|
|
||||||
gridBounds.width,
|
|
||||||
gridBounds.height
|
|
||||||
),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[tiles, gridBounds]
|
|
||||||
);
|
|
||||||
|
|
||||||
const bind = useDrag(
|
|
||||||
({ args: [key], active, xy, movement, tap, event }) => {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
if (tap) {
|
|
||||||
onTap(key);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const dragTileIndex = tiles.findIndex((tile) => tile.key === key);
|
|
||||||
const dragTile = tiles[dragTileIndex];
|
|
||||||
const dragTilePosition = tilePositions[dragTileIndex];
|
|
||||||
|
|
||||||
let newTiles = tiles;
|
|
||||||
|
|
||||||
const cursorPosition = [xy[0] - gridBounds.left, xy[1] - gridBounds.top];
|
|
||||||
|
|
||||||
for (
|
|
||||||
let hoverTileIndex = 0;
|
|
||||||
hoverTileIndex < tiles.length;
|
|
||||||
hoverTileIndex++
|
|
||||||
) {
|
|
||||||
const hoverTile = tiles[hoverTileIndex];
|
|
||||||
const hoverTilePosition = tilePositions[hoverTileIndex];
|
|
||||||
|
|
||||||
if (hoverTile.key === key) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isInside(cursorPosition, hoverTilePosition)) {
|
|
||||||
newTiles = moveArrItem(tiles, dragTileIndex, hoverTileIndex);
|
|
||||||
|
|
||||||
newTiles = newTiles.map((tile) => {
|
|
||||||
if (tile === hoverTile) {
|
|
||||||
return { ...tile, presenter: dragTile.presenter };
|
|
||||||
} else if (tile === dragTile) {
|
|
||||||
return { ...tile, presenter: hoverTile.presenter };
|
|
||||||
} else {
|
|
||||||
return tile;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
newTiles.sort(
|
|
||||||
(a, b) => (b.presenter ? 1 : 0) - (a.presenter ? 1 : 0)
|
|
||||||
);
|
|
||||||
|
|
||||||
setTileState((state) => ({ ...state, tiles: newTiles }));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (active) {
|
|
||||||
if (!draggingTileRef.current) {
|
|
||||||
draggingTileRef.current = {
|
|
||||||
key: dragTile.key,
|
|
||||||
offsetX: dragTilePosition.x,
|
|
||||||
offsetY: dragTilePosition.y,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
draggingTileRef.current.x = movement[0];
|
|
||||||
draggingTileRef.current.y = movement[1];
|
|
||||||
} else {
|
|
||||||
draggingTileRef.current = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
api.start(animate(newTiles));
|
|
||||||
},
|
|
||||||
{ filterTaps: true, enabled: layout === "gallery" }
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.grid} ref={gridRef}>
|
|
||||||
{springs.map(({ shadow, ...style }, i) => {
|
|
||||||
const tile = tiles[i];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ParticipantTile
|
|
||||||
{...bind(tile.key)}
|
|
||||||
key={tile.key}
|
|
||||||
style={{
|
|
||||||
boxShadow: shadow.to(
|
|
||||||
(s) => `rgba(0, 0, 0, 0.5) 0px ${s}px ${2 * s}px 0px`
|
|
||||||
),
|
|
||||||
...style,
|
|
||||||
}}
|
|
||||||
{...tile}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
VideoGrid.defaultProps = {
|
|
||||||
layout: "gallery",
|
|
||||||
};
|
|
||||||
|
|
||||||
function ParticipantTile({ style, participant, remove, presenter, ...rest }) {
|
|
||||||
const videoRef = useRef();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (participant.usermediaStream) {
|
|
||||||
// Mute the local video
|
|
||||||
// TODO: Should GroupCallParticipant have a local field?
|
|
||||||
if (!participant.call) {
|
|
||||||
videoRef.current.muted = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
videoRef.current.srcObject = participant.usermediaStream;
|
|
||||||
videoRef.current.play();
|
|
||||||
} else {
|
|
||||||
videoRef.current.srcObject = null;
|
|
||||||
}
|
|
||||||
}, [participant.usermediaStream]);
|
|
||||||
|
|
||||||
// Firefox doesn't respect the disablePictureInPicture attribute
|
|
||||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1611831
|
|
||||||
|
|
||||||
return (
|
|
||||||
<animated.div className={styles.participantTile} style={style} {...rest}>
|
|
||||||
<div
|
|
||||||
className={classNames(styles.participantName, {
|
|
||||||
[styles.speaking]: participant.usermediaFeed?.isSpeaking(),
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{participant.usermediaFeed?.isSpeaking() ? (
|
|
||||||
<MicIcon />
|
|
||||||
) : participant.isAudioMuted() ? (
|
|
||||||
<MuteMicIcon className={styles.muteMicIcon} />
|
|
||||||
) : null}
|
|
||||||
<span>{participant.member.rawDisplayName}</span>
|
|
||||||
</div>
|
|
||||||
{participant.videoMuted && (
|
|
||||||
<DisableVideoIcon
|
|
||||||
className={styles.videoMuted}
|
|
||||||
width={48}
|
|
||||||
height={48}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<video ref={videoRef} playsInline disablePictureInPicture />
|
|
||||||
</animated.div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,86 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2021 New Vector Ltd
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.grid {
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.participantTile {
|
|
||||||
position: absolute;
|
|
||||||
will-change: transform, width, height, opacity, box-shadow;
|
|
||||||
border-radius: 24px;
|
|
||||||
overflow: hidden;
|
|
||||||
background-color: #444;
|
|
||||||
}
|
|
||||||
|
|
||||||
.participantTile * {
|
|
||||||
touch-action: none;
|
|
||||||
-moz-user-select: none;
|
|
||||||
-webkit-user-drag: none;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.participantTile video {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
.participantName {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 16px;
|
|
||||||
left: 16px;
|
|
||||||
height: 32px;
|
|
||||||
padding: 0 8px;
|
|
||||||
background-color: rgba(23, 25, 28, 0.96);
|
|
||||||
display: none;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border-radius: 8px;
|
|
||||||
user-select: none;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.participantName > * {
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.participantName > :last-child {
|
|
||||||
margin-right: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.participantName span {
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.muteMicIcon * {
|
|
||||||
fill: #FF5B55;
|
|
||||||
}
|
|
||||||
|
|
||||||
.videoMuted {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.participantTile:hover .participantName, .participantName.speaking {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
17
tsconfig.json
Normal file
17
tsconfig.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "commonjs",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"target": "es2016",
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"sourceMap": false,
|
||||||
|
"declaration": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react",
|
||||||
|
"lib": ["es2019", "dom", "dom.iterable"]
|
||||||
|
}
|
||||||
|
}
|
@ -15,12 +15,11 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import reactRefresh from "@vitejs/plugin-react-refresh";
|
|
||||||
import svgrPlugin from "vite-plugin-svgr";
|
import svgrPlugin from "vite-plugin-svgr";
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [reactRefresh(), svgrPlugin()],
|
plugins: [svgrPlugin()],
|
||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
"/_matrix": "http://localhost:8008",
|
"/_matrix": "http://localhost:8008",
|
||||||
|
Loading…
Reference in New Issue
Block a user