From f9d799ff0586b163b158d36e3f9fffb69ab83a5f Mon Sep 17 00:00:00 2001 From: Robert Long Date: Wed, 11 Aug 2021 16:02:40 -0700 Subject: [PATCH] Basic CSS Grid based video grid demo --- package-lock.json | 130 +++++++++++++++++++++++++++++++++++++++- package.json | 4 +- src/App.jsx | 4 ++ src/GridDemo.jsx | 90 ++++++++++++++++++++++++++++ src/GridDemo.module.css | 32 ++++++++++ 5 files changed, 258 insertions(+), 2 deletions(-) create mode 100644 src/GridDemo.jsx create mode 100644 src/GridDemo.module.css diff --git a/package-lock.json b/package-lock.json index 2d8f52d7..4e82ee0a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,13 +7,15 @@ "": { "version": "0.0.0", "dependencies": { + "@react-spring/web": "^9.2.4", "classnames": "^2.3.1", "color-hash": "^2.0.1", "events": "^3.3.0", "matrix-js-sdk": "^12.0.1", "react": "^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" }, "devDependencies": { "@vitejs/plugin-react-refresh": "^1.3.1", @@ -390,6 +392,69 @@ "node": ">=6.9.0" } }, + "node_modules/@react-spring/animated": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.2.4.tgz", + "integrity": "sha512-AfV6ZM8pCCAT29GY5C8/1bOPjZrv/7kD0vedjiE/tEYvNDwg9GlscrvsTViWR2XykJoYrDfdkYArrldWpsCJ5g==", + "dependencies": { + "@react-spring/shared": "~9.2.0", + "@react-spring/types": "~9.2.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0" + } + }, + "node_modules/@react-spring/core": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.2.4.tgz", + "integrity": "sha512-R+PwyfsjiuYCWqaTTfCpYpRmsP0h87RNm7uxC1Uxy7QAHUfHEm2sAHn+AdHPwq/MbVwDssVT8C5yf2WGcqiXGg==", + "hasInstallScript": true, + "dependencies": { + "@react-spring/animated": "~9.2.0", + "@react-spring/shared": "~9.2.0", + "@react-spring/types": "~9.2.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0" + } + }, + "node_modules/@react-spring/rafz": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.2.4.tgz", + "integrity": "sha512-SOKf9eue+vAX+DGo7kWYNl9i9J3gPUlQjifIcV9Bzw9h3i30wPOOP0TjS7iMG/kLp2cdHQYDNFte6nt23VAZkQ==" + }, + "node_modules/@react-spring/shared": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.2.4.tgz", + "integrity": "sha512-ZEr4l2BxmyFRUvRA2VCkPfCJii4E7cGkwbjmTBx1EmcGrOnde/V2eF5dxqCTY3k35QuCegkrWe0coRJVkh8q2Q==", + "dependencies": { + "@react-spring/rafz": "~9.2.0", + "@react-spring/types": "~9.2.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0" + } + }, + "node_modules/@react-spring/types": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.2.4.tgz", + "integrity": "sha512-zHUXrWO8nweUN/ISjrjqU7GgXXvoEbFca1CgiE0TY0H/dqJb3l+Rhx8ecPVNYimzFg3ZZ1/T0egpLop8SOv4aA==" + }, + "node_modules/@react-spring/web": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/@react-spring/web/-/web-9.2.4.tgz", + "integrity": "sha512-vtPvOalLFvuju/MDBtoSnCyt0xXSL6Amyv82fljOuWPl1yGd4M1WteijnYL9Zlriljl0a3oXcPunAVYTD9dbDQ==", + "dependencies": { + "@react-spring/animated": "~9.2.0", + "@react-spring/core": "~9.2.0", + "@react-spring/shared": "~9.2.0", + "@react-spring/types": "~9.2.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0", + "react-dom": "^16.8.0 || ^17.0.0" + } + }, "node_modules/@rollup/pluginutils": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.1.0.tgz", @@ -1328,6 +1393,14 @@ "react": ">=15" } }, + "node_modules/react-use-gesture": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/react-use-gesture/-/react-use-gesture-9.1.3.tgz", + "integrity": "sha512-CdqA2SmS/fj3kkS2W8ZU8wjTbVBAIwDWaRprX7OKaj7HlGwBasGEFggmk5qNklknqk9zK/h8D355bEJFTpqEMg==", + "peerDependencies": { + "react": ">= 16.8.0" + } + }, "node_modules/regenerator-runtime": { "version": "0.13.7", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", @@ -1891,6 +1964,55 @@ "to-fast-properties": "^2.0.0" } }, + "@react-spring/animated": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.2.4.tgz", + "integrity": "sha512-AfV6ZM8pCCAT29GY5C8/1bOPjZrv/7kD0vedjiE/tEYvNDwg9GlscrvsTViWR2XykJoYrDfdkYArrldWpsCJ5g==", + "requires": { + "@react-spring/shared": "~9.2.0", + "@react-spring/types": "~9.2.0" + } + }, + "@react-spring/core": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.2.4.tgz", + "integrity": "sha512-R+PwyfsjiuYCWqaTTfCpYpRmsP0h87RNm7uxC1Uxy7QAHUfHEm2sAHn+AdHPwq/MbVwDssVT8C5yf2WGcqiXGg==", + "requires": { + "@react-spring/animated": "~9.2.0", + "@react-spring/shared": "~9.2.0", + "@react-spring/types": "~9.2.0" + } + }, + "@react-spring/rafz": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.2.4.tgz", + "integrity": "sha512-SOKf9eue+vAX+DGo7kWYNl9i9J3gPUlQjifIcV9Bzw9h3i30wPOOP0TjS7iMG/kLp2cdHQYDNFte6nt23VAZkQ==" + }, + "@react-spring/shared": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.2.4.tgz", + "integrity": "sha512-ZEr4l2BxmyFRUvRA2VCkPfCJii4E7cGkwbjmTBx1EmcGrOnde/V2eF5dxqCTY3k35QuCegkrWe0coRJVkh8q2Q==", + "requires": { + "@react-spring/rafz": "~9.2.0", + "@react-spring/types": "~9.2.0" + } + }, + "@react-spring/types": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.2.4.tgz", + "integrity": "sha512-zHUXrWO8nweUN/ISjrjqU7GgXXvoEbFca1CgiE0TY0H/dqJb3l+Rhx8ecPVNYimzFg3ZZ1/T0egpLop8SOv4aA==" + }, + "@react-spring/web": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/@react-spring/web/-/web-9.2.4.tgz", + "integrity": "sha512-vtPvOalLFvuju/MDBtoSnCyt0xXSL6Amyv82fljOuWPl1yGd4M1WteijnYL9Zlriljl0a3oXcPunAVYTD9dbDQ==", + "requires": { + "@react-spring/animated": "~9.2.0", + "@react-spring/core": "~9.2.0", + "@react-spring/shared": "~9.2.0", + "@react-spring/types": "~9.2.0" + } + }, "@rollup/pluginutils": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.1.0.tgz", @@ -2610,6 +2732,12 @@ "tiny-warning": "^1.0.0" } }, + "react-use-gesture": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/react-use-gesture/-/react-use-gesture-9.1.3.tgz", + "integrity": "sha512-CdqA2SmS/fj3kkS2W8ZU8wjTbVBAIwDWaRprX7OKaj7HlGwBasGEFggmk5qNklknqk9zK/h8D355bEJFTpqEMg==", + "requires": {} + }, "regenerator-runtime": { "version": "0.13.7", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", diff --git a/package.json b/package.json index 5e4612b2..c06ad03d 100644 --- a/package.json +++ b/package.json @@ -6,13 +6,15 @@ "serve": "vite preview" }, "dependencies": { + "@react-spring/web": "^9.2.4", "classnames": "^2.3.1", "color-hash": "^2.0.1", "events": "^3.3.0", "matrix-js-sdk": "^12.0.1", "react": "^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" }, "devDependencies": { "@vitejs/plugin-react-refresh": "^1.3.1", diff --git a/src/App.jsx b/src/App.jsx index fb5e3c34..67ef05ba 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -26,6 +26,7 @@ import { useConferenceCallManager } from "./ConferenceCallManagerHooks"; import { JoinOrCreateRoom } from "./JoinOrCreateRoom"; import { LoginOrRegister } from "./LoginOrRegister"; import { Room } from "./Room"; +import { GridDemo } from "./GridDemo"; export default function App() { const { protocol, host } = window.location; @@ -55,6 +56,9 @@ export default function App() { > + + + )} diff --git a/src/GridDemo.jsx b/src/GridDemo.jsx new file mode 100644 index 00000000..69652d88 --- /dev/null +++ b/src/GridDemo.jsx @@ -0,0 +1,90 @@ +import React, { useCallback, useEffect, useRef, useState } from "react"; +import classNames from "classnames"; +import { useDrag } from "react-use-gesture"; +import { useSpring, useTransition, animated } from "@react-spring/web"; +import styles from "./GridDemo.module.css"; + +let tileIdx = 0; + +export function GridDemo() { + const [stream, setStream] = useState(); + const [tiles, setTiles] = useState([]); + + const startWebcam = useCallback(async () => { + const stream = await navigator.mediaDevices.getUserMedia({ video: true }); + setStream(stream); + setTiles([{ stream, key: tileIdx++ }]); + }, []); + + const addTile = useCallback(() => { + const newStream = stream.clone(); + setTiles((tiles) => [...tiles, { stream: newStream, key: tileIdx++ }]); + }, [stream]); + + const removeTile = useCallback(() => { + setTiles((tiles) => { + const newArr = [...tiles]; + newArr.pop(); + return newArr; + }); + }, []); + + useEffect(() => { + console.log(tiles); + }, [tiles]); + + const tileTransitions = useTransition(tiles, { + from: { opacity: 0, scale: 0.5 }, + enter: { opacity: 1, scale: 1 }, + leave: { opacity: 0, scale: 0.5 }, + }); + + return ( +
+
+ {!stream && } + {stream && } + {stream && } +
+
+ {tileTransitions((style, tile) => ( + + ))} +
+
+ ); +} + +function ParticipantTile({ style, stream }) { + const videoRef = useRef(); + + useEffect(() => { + if (stream) { + videoRef.current.srcObject = stream; + videoRef.current.play(); + } else { + videoRef.current.srcObject = null; + } + }, [stream]); + + const [{ x, y }, api] = useSpring(() => ({ + from: { x: 0, y: 0 }, + config: { + tension: 250, + }, + })); + + const bind = useDrag(({ down, movement: [mx, my] }) => { + api.start({ x: down ? mx : 0, y: down ? my : 0 }); + }); + + return ( + + + ); +} diff --git a/src/GridDemo.module.css b/src/GridDemo.module.css new file mode 100644 index 00000000..a655e524 --- /dev/null +++ b/src/GridDemo.module.css @@ -0,0 +1,32 @@ +.gridDemo { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + overflow: hidden; + display: flex; + flex-direction: column; +} + +.grid { + position: relative; + overflow: hidden; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + grid-auto-flow: dense; + grid-gap: 8px; + justify-items: stretch; + padding: 8px; + flex: 1; +} + +.participantTile { + will-change: transform, opacity; +} + +.participantTile video { + width: 100%; + height: 100%; + object-fit: cover; +} \ No newline at end of file