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
This commit is contained in:
parent
91a8995a98
commit
664a57d4eb
2 changed files with 58 additions and 4 deletions
|
|
@ -31,6 +31,45 @@ export const LIVEKIT_PARTICIPANTS = 39004
|
||||||
|
|
||||||
export {supportsAudioOutputSelection}
|
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 LIVEKIT_DEFAULT_DEVICE_ID = "default"
|
||||||
const VISUAL_SOURCES = [Track.Source.Camera, Track.Source.ScreenShare] as const
|
const VISUAL_SOURCES = [Track.Source.Camera, Track.Source.ScreenShare] as const
|
||||||
const RECONNECT_DELAYS = [1000, 2000, 4000, 8000, 16000]
|
const RECONNECT_DELAYS = [1000, 2000, 4000, 8000, 16000]
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@
|
||||||
participantMediaState,
|
participantMediaState,
|
||||||
pubkeyFromLiveKitIdentity,
|
pubkeyFromLiveKitIdentity,
|
||||||
videoTrackRevision,
|
videoTrackRevision,
|
||||||
|
computeGridSize,
|
||||||
|
type GridSize,
|
||||||
} from "@app/call"
|
} from "@app/call"
|
||||||
import {profiles} from "@app/core"
|
import {profiles} from "@app/core"
|
||||||
|
|
||||||
|
|
@ -173,10 +175,19 @@
|
||||||
return videoTiles.filter(t => tileKey(t) !== pk)
|
return videoTiles.filter(t => tileKey(t) !== pk)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
let gridWidth = $state(0)
|
||||||
|
let gridHeight = $state(0)
|
||||||
|
|
||||||
const useSpotlightLayout = $derived(primaryTile !== undefined)
|
const useSpotlightLayout = $derived(primaryTile !== undefined)
|
||||||
const useMultiGrid = $derived(!useSpotlightLayout && videoTiles.length > 2)
|
const useMultiGrid = $derived(!useSpotlightLayout)
|
||||||
const multiGridClass = $derived(
|
|
||||||
layout === VideoCallLayout.Split ? "grid-cols-1" : "grid-cols-1 sm:grid-cols-2",
|
const gridSize = $derived<GridSize>(
|
||||||
|
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(() => {
|
$effect(() => {
|
||||||
|
|
@ -283,7 +294,11 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else if useMultiGrid}
|
{:else if useMultiGrid}
|
||||||
<div class={cx("grid min-h-0 flex-1 content-start gap-2 overflow-y-auto", multiGridClass)}>
|
<div
|
||||||
|
bind:clientWidth={gridWidth}
|
||||||
|
bind:clientHeight={gridHeight}
|
||||||
|
style={gridStyle}
|
||||||
|
class="grid min-h-0 flex-1 content-start gap-2 overflow-y-auto">
|
||||||
{#each videoTiles as tile (tileKey(tile))}
|
{#each videoTiles as tile (tileKey(tile))}
|
||||||
{@render videoTile(tile, "default")}
|
{@render videoTile(tile, "default")}
|
||||||
{/each}
|
{/each}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue