diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index 402b9cb..048ec6a 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -440,19 +440,29 @@ function useParticipantTiles( }) ); - const someoneIsPresenting = sfuParticipants.some((p) => { - !p.isLocal && p.isScreenShareEnabled; - }); + const hasPresenter = + sfuParticipants.find((p) => p.isScreenShareEnabled) !== undefined; + const speakActiveTime = new Date(); + speakActiveTime.setSeconds(speakActiveTime.getSeconds() - 10); // Iterate over SFU participants (those who actually are present from the SFU perspective) and create tiles for them. const tiles: TileDescriptor[] = sfuParticipants.flatMap( (sfuParticipant) => { + const hadSpokedInTime = + !hasPresenter && sfuParticipant.lastSpokeAt + ? sfuParticipant.lastSpokeAt > speakActiveTime + : false; + const id = sfuParticipant.identity; const member = matrixParticipants.get(id); - const userMediaTile = { id, - focused: !someoneIsPresenting && sfuParticipant.isSpeaking, + focused: false, + isPresenter: sfuParticipant.isScreenShareEnabled, + isSpeaker: + (sfuParticipant.isSpeaking || hadSpokedInTime) && + !sfuParticipant.isLocal, + hasVideo: sfuParticipant.isCameraEnabled, local: sfuParticipant.isLocal, data: { member, diff --git a/src/video-grid/VideoGrid.tsx b/src/video-grid/VideoGrid.tsx index 63c0c62..bc572db 100644 --- a/src/video-grid/VideoGrid.tsx +++ b/src/video-grid/VideoGrid.tsx @@ -57,6 +57,9 @@ interface Tile { item: TileDescriptor; remove: boolean; focused: boolean; + isPresenter: boolean; + isSpeaker: boolean; + hasVideo: boolean; } export interface TileSpring { @@ -688,9 +691,41 @@ function getSubGridPositions( return newTilePositions; } +// Calculates the number of possible tiles that can be displayed +function displayedTileCount( + layout: Layout, + tileCount, + gridWidth: number, + gridHeight: number +): number { + let displayedTile = -1; + if (layout === "freedom") { + return displayedTile; + } + if (tileCount < 2) { + return displayedTile; + } + + const gridAspectRatio = gridWidth / gridHeight; + + if (gridAspectRatio < 1) { + // Vertical layout (mobile) + const spotlightTileHeight = (gridHeight - GAP * 3) * (4 / 5); + const spectatorTileSize = gridHeight - GAP * 3 - spotlightTileHeight; + displayedTile = Math.round(gridWidth / spectatorTileSize); + } else { + const spotlightTileWidth = ((gridWidth - GAP * 3) * 4) / 5; + const spectatorTileWidth = gridWidth - GAP * 3 - spotlightTileWidth; + const spectatorTileHeight = spectatorTileWidth * (9 / 16); + displayedTile = Math.round(gridHeight / spectatorTileHeight); + } + + return displayedTile; +} + // Sets the 'order' property on tiles based on the layout param and // other properties of the tiles, eg. 'focused' and 'presenter' -function reorderTiles(tiles: Tile[], layout: Layout) { +function reorderTiles(tiles: Tile[], layout: Layout, displayedTile = -1) { // We use a special layout for 1:1 to always put the local tile first. // We only do this if there are two tiles (obviously) and exactly one // of them is local: during startup we can have tiles from other users @@ -707,16 +742,35 @@ function reorderTiles(tiles: Tile[], layout: Layout) { tiles.forEach((tile) => (tile.order = tile.item.local ? 0 : 1)); } else { const focusedTiles: Tile[] = []; + const presenterTiles: Tile[] = []; + const speakerTiles: Tile[] = []; + const onlyVideoTiles: Tile[] = []; const otherTiles: Tile[] = []; const orderedTiles: Tile[] = new Array(tiles.length); tiles.forEach((tile) => (orderedTiles[tile.order] = tile)); - orderedTiles.forEach((tile) => - (tile.focused ? focusedTiles : otherTiles).push(tile) - ); + orderedTiles.forEach((tile) => { + if (tile.focused) { + focusedTiles.push(tile); + } else if (tile.isPresenter) { + presenterTiles.push(tile); + } else if (tile.isSpeaker && displayedTile < tile.order) { + speakerTiles.push(tile); + } else if (tile.hasVideo) { + onlyVideoTiles.push(tile); + } else { + otherTiles.push(tile); + } + }); - [...focusedTiles, ...otherTiles].forEach((tile, i) => (tile.order = i)); + [ + ...focusedTiles, + ...presenterTiles, + ...speakerTiles, + ...onlyVideoTiles, + ...otherTiles, + ].forEach((tile, i) => (tile.order = i)); } } @@ -754,6 +808,9 @@ export interface VideoGridProps { export interface TileDescriptor { id: string; focused: boolean; + isPresenter: boolean; + isSpeaker: boolean; + hasVideo: boolean; local: boolean; data: T; } @@ -806,10 +863,19 @@ export function VideoGrid({ } let focused: boolean; + let isSpeaker: boolean; + let isPresenter: boolean; + let hasVideo: boolean; if (layout === "spotlight") { focused = item.focused; + isPresenter = item.isPresenter; + isSpeaker = item.isSpeaker; + hasVideo = item.hasVideo; } else { focused = layout === lastLayoutRef.current ? tile.focused : false; + isPresenter = false; + isSpeaker = false; + hasVideo = false; } newTiles.push({ @@ -818,6 +884,9 @@ export function VideoGrid({ item, remove, focused, + isSpeaker: isSpeaker, + isPresenter: isPresenter, + hasVideo: hasVideo, }); } @@ -838,6 +907,9 @@ export function VideoGrid({ item, remove: false, focused: layout === "spotlight" && item.focused, + isPresenter: item.isPresenter, + isSpeaker: item.isSpeaker, + hasVideo: item.hasVideo, }; if (existingTile) { @@ -849,7 +921,19 @@ export function VideoGrid({ } } - reorderTiles(newTiles, layout); + const presenter = newTiles.find((t) => t.isPresenter); + let displayedTile = -1; + // Only on screen share we will not move active displayed speaker + if (presenter !== undefined) { + displayedTile = displayedTileCount( + layout, + newTiles.length, + gridBounds.width, + gridBounds.height + ); + } + + reorderTiles(newTiles, layout, displayedTile); if (removedTileKeys.size > 0) { setTimeout(() => {