diff --git a/src/GridDemo.jsx b/src/GridDemo.jsx index 6e653dd..15cc863 100644 --- a/src/GridDemo.jsx +++ b/src/GridDemo.jsx @@ -1,344 +1,6 @@ -import React, { useCallback, useEffect, useRef, useState } from "react"; -import { useDrag } from "react-use-gesture"; -import { useSprings, animated } from "@react-spring/web"; +import React, { useCallback, useRef, useState } from "react"; import styles from "./GridDemo.module.css"; -import useMeasure from "react-use-measure"; -import moveArrItem from "lodash-move"; - -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, gridBounds) { - const newTilePositions = []; - const { width: gridWidth, height: gridHeight } = gridBounds; - const gap = 8; - - if (tileCount > 0) { - const aspectRatio = gridWidth / gridHeight; - - let columnCount, rowCount; - - if (aspectRatio < 1) { - if (tileCount <= 4) { - columnCount = 1; - rowCount = tileCount; - } else if (tileCount <= 12) { - columnCount = 2; - rowCount = Math.ceil(tileCount / 2); - } - } else { - 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 <= 10) { - columnCount = 5; - rowCount = 2; - } else if (tileCount <= 12) { - columnCount = 4; - rowCount = 3; - } - } - - let tileHeight = Math.round((gridHeight - gap * (rowCount + 1)) / rowCount); - let tileWidth = Math.round( - (gridWidth - gap * (columnCount + 1)) / columnCount - ); - - const tileAspectRatio = tileWidth / tileHeight; - - if (tileAspectRatio > 16 / 9) { - tileWidth = (16 * tileHeight) / 9; - } - - for (let i = 0; i < tileCount; i++) { - const verticalIndex = Math.floor(i / columnCount); - const top = verticalIndex * tileHeight + (verticalIndex + 1) * gap; - - let rowItemCount; - - if (verticalIndex + 1 === rowCount && tileCount % rowCount !== 0) { - rowItemCount = Math.floor(tileCount / rowCount); - } else { - rowItemCount = Math.ceil(tileCount / rowCount); - } - - const horizontalIndex = i % columnCount; - const totalRowGapWidth = (rowItemCount + 1) * gap; - const totalRowTileWidth = rowItemCount * tileWidth; - const rowLeftMargin = Math.round( - (gridWidth - (totalRowTileWidth + totalRowGapWidth)) / 2 - ); - const left = - tileWidth * horizontalIndex + - rowLeftMargin + - (horizontalIndex + 1) * gap; - - newTilePositions.push({ - width: tileWidth, - height: tileHeight, - x: left, - y: top, - }); - } - } - - return newTilePositions; -} - -export function VideoGrid({ participants }) { - const [{ tiles, tilePositions }, setTileState] = useState({ - tiles: [], - tilePositions: [], - }); - const draggingTileRef = useRef(null); - - // Contains tile indices - // Tiles are displayed in the order that they appear - const tileOrderRef = useRef([]); - - const [gridRef, gridBounds] = useMeasure(); - - useEffect(() => { - setTileState(({ tiles }) => { - const newTiles = []; - const removedTileKeys = []; - - for (const tile of tiles) { - const participant = participants.find( - (participant) => participant.userId === tile.key - ); - - if (participant) { - // Existing tiles - newTiles.push({ - key: participant.userId, - participant: participant, - remove: false, - }); - } else { - // Removed tiles - removedTileKeys.push(tile.key); - newTiles.push({ - key: tile.key, - participant: tile.participant, - remove: true, - }); - } - } - - for (const participant of participants) { - if (newTiles.some(({ key }) => participant.userId === key)) { - continue; - } - - // Added tiles - newTiles.push({ - key: participant.userId, - participant, - remove: false, - }); - - tileOrderRef.current.push(tileOrderRef.current.length); - } - - if (removedTileKeys.length > 0) { - // TODO: There's a race condition in this nested set state when you quickly add/remove - setTimeout(() => { - setTileState(({ tiles }) => { - const newTiles = tiles.filter( - (tile) => !removedTileKeys.includes(tile.key) - ); - const removedTileIndices = removedTileKeys.map((tileKey) => - tiles.findIndex((tile) => tile.key === tileKey) - ); - tileOrderRef.current = tileOrderRef.current.filter( - (index) => !removedTileIndices.includes(index) - ); - - return { - tiles: newTiles, - tilePositions: getTilePositions(newTiles, gridBounds), - }; - }); - }, 250); - } - - return { - tiles: newTiles, - tilePositions: getTilePositions(newTiles.length, gridBounds), - }; - }); - }, [participants, gridBounds]); - - const animate = useCallback( - (tileIndex) => { - const tileOrder = tileOrderRef.current; - const order = tileOrder.indexOf(tileIndex); - const tile = tiles[tileIndex]; - const tilePosition = tilePositions[order]; - 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, - immediate: false, - from: { - scale: 0, - opacity: 0, - }, - reset: false, - }; - } - }, - [tiles, tilePositions] - ); - - const [springs, api] = useSprings(tiles.length, animate, [ - tilePositions, - tiles, - ]); - - const bind = useDrag(({ args: [key], active, xy, movement }) => { - const tileOrder = tileOrderRef.current; - - const dragTileIndex = tiles.findIndex((tile) => tile.key === key); - const dragTile = tiles[dragTileIndex]; - - const dragTileOrder = tileOrder.indexOf(dragTileIndex); - - const cursorPosition = [xy[0] - gridBounds.left, xy[1] - gridBounds.top]; - - for ( - let hoverTileIndex = 0; - hoverTileIndex < tiles.length; - hoverTileIndex++ - ) { - const hoverTile = tiles[hoverTileIndex]; - const hoverTileOrder = tileOrder.indexOf(hoverTileIndex); - const hoverTilePosition = tilePositions[hoverTileOrder]; - - if (hoverTile.key === key) { - continue; - } - - if (isInside(cursorPosition, hoverTilePosition)) { - tileOrderRef.current = moveArrItem( - tileOrder, - dragTileOrder, - hoverTileOrder - ); - break; - } - } - - if (active) { - if (!draggingTileRef.current) { - const tilePosition = tilePositions[dragTileOrder]; - - draggingTileRef.current = { - key: dragTile.key, - offsetX: tilePosition.x, - offsetY: tilePosition.y, - }; - } - - draggingTileRef.current.x = movement[0]; - draggingTileRef.current.y = movement[1]; - } else { - draggingTileRef.current = null; - } - - api.start(animate); - }); - - return ( -
- {springs.map(({ shadow, ...style }, i) => { - const tileIndex = tileOrderRef.current[i]; - const tile = tiles[tileIndex]; - - return ( - `rgba(0, 0, 0, 0.5) 0px ${s}px ${2 * s}px 0px` - ), - ...style, - }} - {...tile} - /> - ); - })} -
- ); -} - -function ParticipantTile({ style, participant, remove, ...rest }) { - const videoRef = useRef(); - - useEffect(() => { - if (participant.stream) { - videoRef.current.srcObject = participant.stream; - videoRef.current.play(); - } else { - videoRef.current.srcObject = null; - } - }, [participant.stream]); - - return ( - -
{participant.userId}
-
- ); -} +import { VideoGrid } from "./VideoGrid"; export function GridDemo() { const participantKey = useRef(0); diff --git a/src/Room.jsx b/src/Room.jsx index b3b58fd..7a9bb8b 100644 --- a/src/Room.jsx +++ b/src/Room.jsx @@ -19,7 +19,7 @@ import styles from "./Room.module.css"; import { useParams, useLocation, Link } from "react-router-dom"; import { useVideoRoom } from "./ConferenceCallManagerHooks"; import { DevTools } from "./DevTools"; -import { VideoGrid } from "./GridDemo"; +import { VideoGrid } from "./VideoGrid"; function useQuery() { const location = useLocation(); diff --git a/src/VideoGrid.jsx b/src/VideoGrid.jsx new file mode 100644 index 0000000..5c1ffb7 --- /dev/null +++ b/src/VideoGrid.jsx @@ -0,0 +1,341 @@ +import React, { useCallback, useEffect, useRef, useState } from "react"; +import { useDrag } from "react-use-gesture"; +import { useSprings, animated } from "@react-spring/web"; +import styles from "./GridDemo.module.css"; +import useMeasure from "react-use-measure"; +import moveArrItem from "lodash-move"; + +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, gridBounds) { + const newTilePositions = []; + const { width: gridWidth, height: gridHeight } = gridBounds; + const gap = 8; + + if (tileCount > 0) { + const aspectRatio = gridWidth / gridHeight; + + let columnCount, rowCount; + + if (aspectRatio < 1) { + if (tileCount <= 4) { + columnCount = 1; + rowCount = tileCount; + } else if (tileCount <= 12) { + columnCount = 2; + rowCount = Math.ceil(tileCount / 2); + } + } else { + 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 <= 10) { + columnCount = 5; + rowCount = 2; + } else if (tileCount <= 12) { + columnCount = 4; + rowCount = 3; + } + } + + let tileHeight = Math.round((gridHeight - gap * (rowCount + 1)) / rowCount); + let tileWidth = Math.round( + (gridWidth - gap * (columnCount + 1)) / columnCount + ); + + const tileAspectRatio = tileWidth / tileHeight; + + if (tileAspectRatio > 16 / 9) { + tileWidth = (16 * tileHeight) / 9; + } + + for (let i = 0; i < tileCount; i++) { + const verticalIndex = Math.floor(i / columnCount); + const top = verticalIndex * tileHeight + (verticalIndex + 1) * gap; + + let rowItemCount; + + if (verticalIndex + 1 === rowCount && tileCount % rowCount !== 0) { + rowItemCount = Math.floor(tileCount / rowCount); + } else { + rowItemCount = Math.ceil(tileCount / rowCount); + } + + const horizontalIndex = i % columnCount; + const totalRowGapWidth = (rowItemCount + 1) * gap; + const totalRowTileWidth = rowItemCount * tileWidth; + const rowLeftMargin = Math.round( + (gridWidth - (totalRowTileWidth + totalRowGapWidth)) / 2 + ); + const left = + tileWidth * horizontalIndex + + rowLeftMargin + + (horizontalIndex + 1) * gap; + + newTilePositions.push({ + width: tileWidth, + height: tileHeight, + x: left, + y: top, + }); + } + } + + return newTilePositions; +} + +export function VideoGrid({ participants }) { + const [{ tiles, tilePositions }, setTileState] = useState({ + tiles: [], + tilePositions: [], + }); + const draggingTileRef = useRef(null); + + // Contains tile indices + // Tiles are displayed in the order that they appear + const tileOrderRef = useRef([]); + + const [gridRef, gridBounds] = useMeasure(); + + useEffect(() => { + setTileState(({ tiles }) => { + const newTiles = []; + const removedTileKeys = []; + + for (const tile of tiles) { + const participant = participants.find( + (participant) => participant.userId === tile.key + ); + + if (participant) { + // Existing tiles + newTiles.push({ + key: participant.userId, + participant: participant, + remove: false, + }); + } else { + // Removed tiles + removedTileKeys.push(tile.key); + newTiles.push({ + key: tile.key, + participant: tile.participant, + remove: true, + }); + } + } + + for (const participant of participants) { + if (newTiles.some(({ key }) => participant.userId === key)) { + continue; + } + + // Added tiles + newTiles.push({ + key: participant.userId, + participant, + remove: false, + }); + + tileOrderRef.current.push(tileOrderRef.current.length); + } + + if (removedTileKeys.length > 0) { + // TODO: There's a race condition in this nested set state when you quickly add/remove + setTimeout(() => { + setTileState(({ tiles }) => { + const newTiles = tiles.filter( + (tile) => !removedTileKeys.includes(tile.key) + ); + const removedTileIndices = removedTileKeys.map((tileKey) => + tiles.findIndex((tile) => tile.key === tileKey) + ); + tileOrderRef.current = tileOrderRef.current.filter( + (index) => !removedTileIndices.includes(index) + ); + + return { + tiles: newTiles, + tilePositions: getTilePositions(newTiles, gridBounds), + }; + }); + }, 250); + } + + return { + tiles: newTiles, + tilePositions: getTilePositions(newTiles.length, gridBounds), + }; + }); + }, [participants, gridBounds]); + + const animate = useCallback( + (tileIndex) => { + const tileOrder = tileOrderRef.current; + const order = tileOrder.indexOf(tileIndex); + const tile = tiles[tileIndex]; + const tilePosition = tilePositions[order]; + 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, + immediate: false, + from: { + scale: 0, + opacity: 0, + }, + reset: false, + }; + } + }, + [tiles, tilePositions] + ); + + const [springs, api] = useSprings(tiles.length, animate, [ + tilePositions, + tiles, + ]); + + const bind = useDrag(({ args: [key], active, xy, movement }) => { + const tileOrder = tileOrderRef.current; + + const dragTileIndex = tiles.findIndex((tile) => tile.key === key); + const dragTile = tiles[dragTileIndex]; + + const dragTileOrder = tileOrder.indexOf(dragTileIndex); + + const cursorPosition = [xy[0] - gridBounds.left, xy[1] - gridBounds.top]; + + for ( + let hoverTileIndex = 0; + hoverTileIndex < tiles.length; + hoverTileIndex++ + ) { + const hoverTile = tiles[hoverTileIndex]; + const hoverTileOrder = tileOrder.indexOf(hoverTileIndex); + const hoverTilePosition = tilePositions[hoverTileOrder]; + + if (hoverTile.key === key) { + continue; + } + + if (isInside(cursorPosition, hoverTilePosition)) { + tileOrderRef.current = moveArrItem( + tileOrder, + dragTileOrder, + hoverTileOrder + ); + break; + } + } + + if (active) { + if (!draggingTileRef.current) { + const tilePosition = tilePositions[dragTileOrder]; + + draggingTileRef.current = { + key: dragTile.key, + offsetX: tilePosition.x, + offsetY: tilePosition.y, + }; + } + + draggingTileRef.current.x = movement[0]; + draggingTileRef.current.y = movement[1]; + } else { + draggingTileRef.current = null; + } + + api.start(animate); + }); + + return ( +
+ {springs.map(({ shadow, ...style }, i) => { + const tileIndex = tileOrderRef.current[i]; + const tile = tiles[tileIndex]; + + return ( + `rgba(0, 0, 0, 0.5) 0px ${s}px ${2 * s}px 0px` + ), + ...style, + }} + {...tile} + /> + ); + })} +
+ ); +} + +function ParticipantTile({ style, participant, remove, ...rest }) { + const videoRef = useRef(); + + useEffect(() => { + if (participant.stream) { + videoRef.current.srcObject = participant.stream; + videoRef.current.play(); + } else { + videoRef.current.srcObject = null; + } + }, [participant.stream]); + + return ( + +
{participant.userId}
+
+ ); +}