From 664a57d4ebd178d435cd15b1098c74961517e1d1 Mon Sep 17 00:00:00 2001 From: Agent Date: Wed, 5 Aug 2026 14:39:01 -0400 Subject: [PATCH] feat: adaptive video grid sizing (PR-C1) Add computeGridSize helper in call.ts that computes optimal row/column counts for a video tile grid given tile count and container dimensions, maximizing tile size while maintaining ~16:9 cells. Apply via computed grid-template-columns/rows on the multi-grid path (no-spotlight) in VideoCallContent.svelte, replacing the fixed grid-cols-1 / grid-cols-1 sm:grid-cols-2 heuristic. The multi-grid now handles all tile counts from 1 to 12+ smoothly. - computeGridSize(tileCount, containerWidth, containerHeight) -> GridSize - Container dimensions bound via clientWidth/clientHeight - useMultiGrid: dropped the > 2 check, now !useSpotlightLayout - gridStyle: computed grid-template-columns/rows string from gridSize - Scope: multi-grid only, no spotlight/strip/breakpoint changes --- src/app/call.ts | 39 ++++++++++++++++++++++ src/app/components/VideoCallContent.svelte | 23 ++++++++++--- 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/src/app/call.ts b/src/app/call.ts index e81c442e..6b4a04f7 100644 --- a/src/app/call.ts +++ b/src/app/call.ts @@ -31,6 +31,45 @@ export const LIVEKIT_PARTICIPANTS = 39004 export {supportsAudioOutputSelection} +export type GridSize = {rows: number; cols: number} + +/** + * Given a tile count and available container dimensions, returns the optimal + * number of rows and columns to maximize tile size while maintaining ~16:9 + * cells. Handles 0 to 12+ tiles smoothly. + */ +export const computeGridSize = ( + tileCount: number, + containerWidth: number, + containerHeight: number, +): GridSize => { + if (tileCount <= 0) return {rows: 0, cols: 0} + if (tileCount === 1) return {rows: 1, cols: 1} + + const TILE_ASPECT = 16 / 9 + let best: GridSize = {rows: 1, cols: tileCount} + let bestArea = 0 + + // Guard against unmeasured container — fall back to a landscape-ish default + const w = containerWidth > 0 ? containerWidth : 800 + const h = containerHeight > 0 ? containerHeight : 600 + + for (let cols = 1; cols <= tileCount; cols++) { + const rows = Math.ceil(tileCount / cols) + // Tile width constrained by both axes while maintaining 16:9 + const tileWidth = Math.min(w / cols, (h / rows) * TILE_ASPECT) + const tileHeight = tileWidth / TILE_ASPECT + const area = tileWidth * tileHeight + + if (area > bestArea) { + bestArea = area + best = {rows, cols} + } + } + + return best +} + const LIVEKIT_DEFAULT_DEVICE_ID = "default" const VISUAL_SOURCES = [Track.Source.Camera, Track.Source.ScreenShare] as const const RECONNECT_DELAYS = [1000, 2000, 4000, 8000, 16000] diff --git a/src/app/components/VideoCallContent.svelte b/src/app/components/VideoCallContent.svelte index c5db244b..e6966856 100644 --- a/src/app/components/VideoCallContent.svelte +++ b/src/app/components/VideoCallContent.svelte @@ -24,6 +24,8 @@ participantMediaState, pubkeyFromLiveKitIdentity, videoTrackRevision, + computeGridSize, + type GridSize, } from "@app/call" import {profiles} from "@app/core" @@ -173,10 +175,19 @@ return videoTiles.filter(t => tileKey(t) !== pk) }) + let gridWidth = $state(0) + let gridHeight = $state(0) + const useSpotlightLayout = $derived(primaryTile !== undefined) - const useMultiGrid = $derived(!useSpotlightLayout && videoTiles.length > 2) - const multiGridClass = $derived( - layout === VideoCallLayout.Split ? "grid-cols-1" : "grid-cols-1 sm:grid-cols-2", + const useMultiGrid = $derived(!useSpotlightLayout) + + const gridSize = $derived( + useMultiGrid ? computeGridSize(videoTiles.length, gridWidth, gridHeight) : {rows: 0, cols: 0}, + ) + const gridStyle = $derived( + gridSize.rows > 0 && gridSize.cols > 0 + ? `grid-template-columns: repeat(${gridSize.cols}, 1fr); grid-template-rows: repeat(${gridSize.rows}, 1fr)` + : "", ) $effect(() => { @@ -283,7 +294,11 @@ {/if} {:else if useMultiGrid} -
+
{#each videoTiles as tile (tileKey(tile))} {@render videoTile(tile, "default")} {/each}