diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index 6c2a709..2a62f9d 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -216,6 +216,7 @@ export function InCallView({ focused: screenshareFeeds.length === 0 && callFeed === activeSpeaker, isLocal: member.userId === localUserId && deviceId === localDeviceId, presenter, + isSpeaker: callFeed === activeSpeaker, largeBaseSize: false, connectionState, }); @@ -244,6 +245,7 @@ export function InCallView({ focused: true, isLocal: screenshareFeed.isLocal(), presenter: false, + isSpeaker: screenshareFeed === activeSpeaker, largeBaseSize: true, placeNear: `${member.userId} ${deviceId}`, connectionState, diff --git a/src/video-grid/NewVideoGrid.tsx b/src/video-grid/NewVideoGrid.tsx index c255959..711e70e 100644 --- a/src/video-grid/NewVideoGrid.tsx +++ b/src/video-grid/NewVideoGrid.tsx @@ -46,6 +46,7 @@ import { addItems, tryMoveTile, resize, + promoteSpeakers, } from "./model"; import { TileWrapper } from "./TileWrapper"; @@ -96,6 +97,9 @@ const useGridState = ( const newItems = items.filter((i) => !existingItemIds.has(i.id)); const grid3 = addItems(newItems, grid2); + // Step 4: Promote speakers to the top + promoteSpeakers(grid3); + return { ...grid3, generation: prevGrid.generation + 1 }; }, [columns, items] diff --git a/src/video-grid/TileDescriptor.tsx b/src/video-grid/TileDescriptor.tsx index c215bc6..771c45a 100644 --- a/src/video-grid/TileDescriptor.tsx +++ b/src/video-grid/TileDescriptor.tsx @@ -26,6 +26,7 @@ export interface TileDescriptor { member: RoomMember; focused: boolean; presenter: boolean; + isSpeaker: boolean; callFeed?: CallFeed; isLocal?: boolean; largeBaseSize: boolean; diff --git a/src/video-grid/model.ts b/src/video-grid/model.ts index 15973c4..767ae19 100644 --- a/src/video-grid/model.ts +++ b/src/video-grid/model.ts @@ -668,3 +668,31 @@ export function resize(g: Grid, columns: number): Grid { return fillGaps(result); } + +/** + * Promotes speakers to the first page of the grid. + */ +export function promoteSpeakers(g: Grid) { + // This is all a bit of a hack right now, because we don't know if the designs + // will stick with this approach in the long run + // We assume that 4 rows are probably about 1 page + const firstPageEnd = g.columns * 4; + + for (let from = firstPageEnd; from < g.cells.length; from++) { + const fromCell = g.cells[from]; + // Don't bother trying to promote enlarged tiles + if ( + fromCell?.item.isSpeaker && + fromCell.columns === 1 && + fromCell.rows === 1 + ) { + // Promote this tile by making 10 attempts to place it on the first page + for (let j = 0; j < 10; j++) { + const to = Math.floor(Math.random() * firstPageEnd); + const toCell = g.cells[to]; + if (toCell === undefined || (toCell.columns === 1 && toCell.rows === 1)) + moveTile(g, from, to); + } + } + } +} diff --git a/test/video-grid/model-test.ts b/test/video-grid/model-test.ts index 479f9cb..5b5897f 100644 --- a/test/video-grid/model-test.ts +++ b/test/video-grid/model-test.ts @@ -33,7 +33,7 @@ import { TileDescriptor } from "../../src/video-grid/TileDescriptor"; function mkGrid(spec: string): Grid { const secondNewline = spec.indexOf("\n", 1); const columns = secondNewline === -1 ? spec.length : secondNewline - 1; - const cells = spec.match(/[a-z ]/g) ?? []; + const cells = spec.match(/[a-z ]/g) ?? ([] as string[]); const areas = new Set(cells); areas.delete(" "); // Space represents an empty cell, not an area const grid: Grid = { columns, cells: new Array(cells.length) };