From 1661c5518bbe6e24ecb3102f75a84b9f3d9030cf Mon Sep 17 00:00:00 2001 From: Robert Long <robert@robertlong.me> Date: Thu, 26 Aug 2021 11:03:48 -0700 Subject: [PATCH] Add ability to toggle presenter by clicking name --- src/ConferenceCallManagerHooks.js | 15 ++ src/GridDemo.jsx | 15 +- src/Room.jsx | 6 +- src/VideoGrid.jsx | 387 ++++++++++++++++++++++++------ src/VideoGrid.module.css | 1 + 5 files changed, 354 insertions(+), 70 deletions(-) diff --git a/src/ConferenceCallManagerHooks.js b/src/ConferenceCallManagerHooks.js index 09941b9..145fec7 100644 --- a/src/ConferenceCallManagerHooks.js +++ b/src/ConferenceCallManagerHooks.js @@ -339,6 +339,20 @@ export function useVideoRoom(manager, roomId, timeout = 5000) { setState((prevState) => ({ ...prevState, videoMuted: manager.videoMuted })); }, [manager]); + const togglePresenter = useCallback((selectedParticipant) => { + setState((prevState) => ({ + ...prevState, + participants: prevState.participants.map((participant) => + participant === selectedParticipant + ? { + ...participant, + presenter: !participant.presenter, + } + : participant + ), + })); + }, []); + return { loading, joined, @@ -350,6 +364,7 @@ export function useVideoRoom(manager, roomId, timeout = 5000) { leaveCall, toggleMuteVideo, toggleMuteAudio, + togglePresenter, videoMuted, audioMuted, }; diff --git a/src/GridDemo.jsx b/src/GridDemo.jsx index 15cc863..479849e 100644 --- a/src/GridDemo.jsx +++ b/src/GridDemo.jsx @@ -20,6 +20,19 @@ export function GridDemo() { ]); }, [stream]); + const togglePresenter = useCallback((selectedParticipant) => { + setParticipants((participants) => + participants.map((participant) => + participant === selectedParticipant + ? { + ...participant, + presenter: !participant.presenter, + } + : participant + ) + ); + }, []); + const removeParticipant = useCallback((key) => { setParticipants((participants) => participants.filter((participant) => participant.userId !== key) @@ -43,7 +56,7 @@ export function GridDemo() { </button> )} </div> - <VideoGrid participants={participants} /> + <VideoGrid participants={participants} onClickNameTag={togglePresenter} /> </div> ); } diff --git a/src/Room.jsx b/src/Room.jsx index 4ff6e34..9bd091d 100644 --- a/src/Room.jsx +++ b/src/Room.jsx @@ -48,6 +48,7 @@ export function Room({ manager }) { leaveCall, toggleMuteVideo, toggleMuteAudio, + togglePresenter, videoMuted, audioMuted, } = useVideoRoom(manager, roomId); @@ -111,7 +112,10 @@ export function Room({ manager }) { </div> )} {!loading && room && joined && participants.length > 0 && ( - <VideoGrid participants={participants} /> + <VideoGrid + participants={participants} + onClickNameTag={togglePresenter} + /> )} {!loading && room && joined && ( <div className={styles.footer}> diff --git a/src/VideoGrid.jsx b/src/VideoGrid.jsx index 4f2737a..8891929 100644 --- a/src/VideoGrid.jsx +++ b/src/VideoGrid.jsx @@ -36,18 +36,209 @@ function isInside([x, y], targetTile) { return true; } -function getTilePositions(tileCount, gridBounds) { - const newTilePositions = []; - const { width: gridWidth, height: gridHeight } = gridBounds; - const gap = 8; +function getTilePositions(tileCount, gridBounds, presenterTileCount) { + if (tileCount === 0) { + return []; + } if (tileCount > 12) { console.warn("Over 12 tiles is not currently supported"); } - if (tileCount > 0) { - const gridAspectRatio = gridWidth / gridHeight; + if (presenterTileCount > 3) { + console.warn("Over 3 presenters is not currently supported"); + } + const gridWidth = gridBounds.width; + const gridHeight = gridBounds.height; + const gridAspectRatio = gridWidth / gridHeight; + + if (presenterTileCount) { + const subGridTileCount = tileCount - presenterTileCount; + + let presenterGridWidth, + presenterGridHeight, + presenterColumnCount, + presenterRowCount, + presenterTileAspectRatio; + + let subGridWidth, + subGridHeight, + subGridOffsetLeft, + subGridOffsetTop, + subGridColumnCount, + subGridRowCount, + subGridTileAspectRatio; + + if (gridAspectRatio < 3 / 4) { + // Phone + presenterGridWidth = gridWidth; + presenterColumnCount = 1; + presenterRowCount = presenterTileCount; + presenterTileAspectRatio = 16 / 9; + subGridTileAspectRatio = 16 / 9; + + if (presenterTileCount > 2) { + presenterColumnCount = 2; + presenterRowCount = 2; + presenterTileAspectRatio = 0; + } + + if (subGridTileCount < 3) { + if (presenterTileCount === 1) { + } + subGridColumnCount = presenterTileCount === 1 ? 1 : subGridTileCount; + subGridRowCount = presenterTileCount === 1 ? subGridTileCount : 1; + subGridTileAspectRatio = presenterTileCount === 1 ? 16 / 9 : 0; + } else if (subGridTileCount < 5) { + subGridColumnCount = 2; + subGridRowCount = 2; + } else if (subGridTileCount < 7) { + subGridColumnCount = 2; + subGridRowCount = 3; + } else if (subGridTileCount < 10) { + subGridColumnCount = 3; + subGridRowCount = 3; + } else { + subGridColumnCount = 4; + subGridRowCount = 3; + } + + presenterGridHeight = Math.round( + gridHeight * + (1 - + 1 / + Math.max( + presenterRowCount + 2 - Math.max(subGridRowCount - 1, 0), + 2 + )) + ); + + subGridWidth = gridWidth; + subGridHeight = gridHeight - presenterGridHeight; + subGridOffsetTop = presenterGridHeight; + subGridOffsetLeft = 0; + } else if (gridAspectRatio < 1) { + // Tablet + presenterGridWidth = gridWidth; + presenterColumnCount = 1; + presenterRowCount = presenterTileCount; + presenterTileAspectRatio = 16 / 9; + subGridTileAspectRatio = 16 / 9; + + if (presenterTileCount > 2) { + presenterColumnCount = 2; + presenterRowCount = 2; + presenterTileAspectRatio = 0; + } + + if (subGridTileCount < 3) { + if (presenterTileCount === 1) { + } + subGridColumnCount = presenterTileCount === 1 ? 1 : subGridTileCount; + subGridRowCount = presenterTileCount === 1 ? subGridTileCount : 1; + subGridTileAspectRatio = presenterTileCount === 1 ? 16 / 9 : 0; + } else if (subGridTileCount < 5) { + subGridColumnCount = 2; + subGridRowCount = 2; + } else if (subGridTileCount < 7) { + subGridColumnCount = 2; + subGridRowCount = 3; + } else if (subGridTileCount < 10) { + subGridColumnCount = 3; + subGridRowCount = 3; + } else { + subGridColumnCount = 4; + subGridRowCount = 3; + } + + presenterGridHeight = Math.round( + gridHeight * + (1 - + 1 / + Math.max( + presenterRowCount + 2 - Math.max(subGridRowCount - 1, 0), + 2 + )) + ); + + subGridWidth = gridWidth; + subGridHeight = gridHeight - presenterGridHeight; + subGridOffsetTop = presenterGridHeight; + subGridOffsetLeft = 0; + } else if (gridAspectRatio < 17 / 9) { + // Computer + presenterGridWidth = gridWidth * (2 / 3); + presenterGridHeight = gridHeight; + presenterColumnCount = 1; + presenterRowCount = presenterTileCount; + presenterTileAspectRatio = 0; + + subGridWidth = gridWidth - presenterGridWidth; + subGridHeight = gridHeight; + subGridColumnCount = Math.ceil(subGridTileCount / 6); + subGridRowCount = Math.ceil(subGridTileCount / subGridColumnCount); + subGridOffsetTop = 0; + subGridOffsetLeft = presenterGridWidth; + subGridTileAspectRatio = 16 / 9; + } else if (gridAspectRatio <= 32 / 9) { + // Ultrawide + presenterGridWidth = gridWidth * (2 / 3); + presenterGridHeight = gridHeight; + presenterColumnCount = 1; + presenterRowCount = presenterTileCount; + presenterTileAspectRatio = 16 / 9; + + subGridWidth = gridWidth - presenterGridWidth; + subGridHeight = gridHeight; + subGridColumnCount = Math.ceil(subGridTileCount / 4); + subGridRowCount = Math.ceil(subGridTileCount / subGridColumnCount); + subGridOffsetTop = 0; + subGridOffsetLeft = presenterGridWidth; + subGridTileAspectRatio = 16 / 9; + } else { + // Super Ultrawide + presenterGridWidth = gridWidth * (2 / 3); + presenterGridHeight = gridHeight; + presenterColumnCount = 1; + presenterRowCount = presenterTileCount; + presenterTileAspectRatio = 16 / 9; + + subGridWidth = gridWidth - presenterGridWidth; + subGridHeight = gridHeight; + subGridColumnCount = Math.ceil(subGridTileCount / 3); + subGridRowCount = Math.ceil(subGridTileCount / subGridColumnCount); + subGridOffsetTop = 0; + subGridOffsetLeft = presenterGridWidth; + subGridTileAspectRatio = 16 / 9; + } + + const presenterPositions = getSubGridPositions( + presenterTileCount, + presenterColumnCount, + presenterRowCount, + presenterTileAspectRatio, + { + width: presenterGridWidth, + height: presenterGridHeight, + } + ); + + const subGridPositions = getSubGridPositions( + subGridTileCount, + subGridColumnCount, + subGridRowCount, + subGridTileAspectRatio, + { + width: subGridWidth, + height: subGridHeight, + offsetTop: subGridOffsetTop, + offsetLeft: subGridOffsetLeft, + } + ); + + return [...presenterPositions, ...subGridPositions]; + } else { let columnCount, rowCount; let tileAspectRatio = 16 / 9; @@ -151,81 +342,111 @@ function getTilePositions(tileCount, gridBounds) { } } - const boxWidth = Math.round( - (gridWidth - gap * (columnCount + 1)) / columnCount - ); - const boxHeight = Math.round( - (gridHeight - gap * (rowCount + 1)) / rowCount + return getSubGridPositions( + tileCount, + columnCount, + rowCount, + tileAspectRatio, + gridBounds ); + } +} - let tileWidth, tileHeight; +function getSubGridPositions( + tileCount, + columnCount, + rowCount, + tileAspectRatio, + gridBounds +) { + if (tileCount === 0) { + return []; + } - if (tileAspectRatio) { - const boxAspectRatio = boxWidth / boxHeight; + const newTilePositions = []; + const gridWidth = gridBounds.width; + const gridHeight = gridBounds.height; + const gridOffsetLeft = gridBounds.offsetLeft || 0; + const gridOffsetTop = gridBounds.offsetTop || 0; + const gap = 8; - if (boxAspectRatio > tileAspectRatio) { - tileWidth = boxHeight * tileAspectRatio; - tileHeight = boxHeight; - } else { - tileWidth = boxWidth; - tileHeight = boxWidth / tileAspectRatio; - } + 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 = boxHeight; + tileHeight = boxWidth / tileAspectRatio; + } + } else { + tileWidth = boxWidth; + tileHeight = boxHeight; + } + + const paddingTop = + (gridHeight - tileHeight * rowCount - gap * (rowCount - 1)) / 2; + + const paddingLeft = + (gridWidth - tileWidth * columnCount - gap * (columnCount - 1)) / 2; + + for (let i = 0; i < tileCount; i++) { + const verticalIndex = Math.floor(i / columnCount); + const top = + gridOffsetTop + + verticalIndex * tileHeight + + verticalIndex * gap + + paddingTop; + + let rowItemCount; + + if (verticalIndex + 1 === rowCount && tileCount % columnCount !== 0) { + rowItemCount = tileCount % columnCount; + } else { + rowItemCount = columnCount; } - const paddingTop = - (gridHeight - tileHeight * rowCount - gap * (rowCount - 1)) / 2; + const horizontalIndex = i % columnCount; - const paddingLeft = - (gridWidth - tileWidth * columnCount - gap * (columnCount - 1)) / 2; + let centeringPadding = 0; - for (let i = 0; i < tileCount; i++) { - const verticalIndex = Math.floor(i / columnCount); - const top = verticalIndex * tileHeight + verticalIndex * gap + paddingTop; - - 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) { - centeringPadding = Math.round( - (gridWidth - - (tileWidth * rowItemCount + - (gap * rowItemCount - 1) + - paddingLeft * 2)) / - 2 - ); - } - - const left = - paddingLeft + - centeringPadding + - gap * horizontalIndex + - tileWidth * horizontalIndex; - - newTilePositions.push({ - width: tileWidth, - height: tileHeight, - x: left, - y: top, - }); + if (rowItemCount < columnCount) { + centeringPadding = Math.round( + (gridWidth - + (tileWidth * rowItemCount + + (gap * rowItemCount - 1) + + paddingLeft * 2)) / + 2 + ); } + + const left = + gridOffsetLeft + + paddingLeft + + centeringPadding + + gap * horizontalIndex + + tileWidth * horizontalIndex; + + newTilePositions.push({ + width: tileWidth, + height: tileHeight, + x: left, + y: top, + }); } return newTilePositions; } -export function VideoGrid({ participants }) { +export function VideoGrid({ participants, onClickNameTag }) { const [{ tiles, tilePositions }, setTileState] = useState({ tiles: [], tilePositions: [], @@ -239,6 +460,7 @@ export function VideoGrid({ participants }) { setTileState(({ tiles }) => { const newTiles = []; const removedTileKeys = []; + let presenterTileCount = 0; for (const tile of tiles) { const participant = participants.find( @@ -264,6 +486,10 @@ export function VideoGrid({ participants }) { } for (const participant of participants) { + if (participant.presenter) { + presenterTileCount++; + } + if (newTiles.some(({ key }) => participant.userId === key)) { continue; } @@ -276,6 +502,11 @@ export function VideoGrid({ participants }) { }); } + newTiles.sort( + (a, b) => + (b.participant.presenter ? 1 : 0) - (a.participant.presenter ? 1 : 0) + ); + if (removedTileKeys.length > 0) { setTimeout(() => { if (!isMounted.current) { @@ -289,7 +520,11 @@ export function VideoGrid({ participants }) { return { tiles: newTiles, - tilePositions: getTilePositions(newTiles.length, gridBounds), + tilePositions: getTilePositions( + newTiles.length, + gridBounds, + presenterTileCount + ), }; }); }, 250); @@ -297,7 +532,11 @@ export function VideoGrid({ participants }) { return { tiles: newTiles, - tilePositions: getTilePositions(newTiles.length, gridBounds), + tilePositions: getTilePositions( + newTiles.length, + gridBounds, + presenterTileCount + ), }; }); }, [participants, gridBounds]); @@ -413,6 +652,7 @@ export function VideoGrid({ participants }) { ...style, }} {...tile} + onClickNameTag={onClickNameTag} /> ); })} @@ -420,7 +660,13 @@ export function VideoGrid({ participants }) { ); } -function ParticipantTile({ style, participant, remove, ...rest }) { +function ParticipantTile({ + style, + participant, + remove, + onClickNameTag, + ...rest +}) { const videoRef = useRef(); useEffect(() => { @@ -445,6 +691,11 @@ function ParticipantTile({ style, participant, remove, ...rest }) { className={classNames(styles.participantName, { [styles.speaking]: participant.speaking, })} + onClick={(e) => { + e.preventDefault(); + e.stopPropagation(); + onClickNameTag(participant); + }} > {participant.speaking ? ( <MicIcon /> diff --git a/src/VideoGrid.module.css b/src/VideoGrid.module.css index df8f560..69cf53d 100644 --- a/src/VideoGrid.module.css +++ b/src/VideoGrid.module.css @@ -46,6 +46,7 @@ limitations under the License. justify-content: center; border-radius: 8px; user-select: none; + cursor: pointer; } .participantName > * {