From 0d9e511f4125dbdcca1e58d823247240a0eff32e Mon Sep 17 00:00:00 2001 From: Agent Date: Wed, 12 Aug 2026 09:49:24 -0400 Subject: [PATCH] fix: mixed-row rendering bug and Jitsi-style algorithm refinements --- src/app/call.ts | 53 +++++++++++----------- src/app/components/VideoCallContent.svelte | 5 +- 2 files changed, 30 insertions(+), 28 deletions(-) diff --git a/src/app/call.ts b/src/app/call.ts index 86fb1033..c16ab812 100644 --- a/src/app/call.ts +++ b/src/app/call.ts @@ -85,6 +85,8 @@ export type AdaptiveTileGrid = { type LayoutScore = { /** Penalty for vertical overflow: 0 if none, else huge */ verticalOverflowPenalty: number + /** Penalty for horizontal overflow: 0 if none, else huge */ + horizontalOverflowPenalty: number /** Penalty when tiles fall below MIN_TILE_HEIGHT even while fitting */ minTileHeightPenalty: number /** Negative tile area (we want to maximize area, so negate) */ @@ -98,6 +100,7 @@ type LayoutScore = { const compareScores = (a: LayoutScore, b: LayoutScore): number => { const fields: (keyof LayoutScore)[] = [ "verticalOverflowPenalty", + "horizontalOverflowPenalty", "minTileHeightPenalty", "negativeArea", "whitespace", @@ -125,8 +128,8 @@ const buildUniformRow = ( const rowHeight = availableHeight / rowCount const tileWidth = (containerWidth - (columnCount - 1) * gap) / columnCount const fillAspect = tileWidth / rowHeight - // No upper bound — allow wider than 16:9 to avoid overflow - const aspectRatio = Math.max(TILE_ASPECT_PORTRAIT, fillAspect) + // Cap at 16:9 to avoid horizontally stretching video (review feedback) + const aspectRatio = Math.max(TILE_ASPECT_PORTRAIT, Math.min(TILE_ASPECT_LANDSCAPE, fillAspect)) const tileHeight = tileWidth / aspectRatio return {columnCount, tileWidth, tileHeight, aspectRatio} } @@ -160,31 +163,21 @@ const buildMixedCandidate = ( const tileWidthLast = (containerWidth - (remainder - 1) * gap) / remainder if (tileWidthLast <= 0) return undefined - // Last row at 16:9 (widest aspect, per review: "single centered 16:9 tile") - const lastRowAspect = TILE_ASPECT_LANDSCAPE - const commonHeight = tileWidthLast / lastRowAspect + // Find a common height that fits both row types and container + const maxPossibleHeight = Math.min( + tileWidthFull / TILE_ASPECT_PORTRAIT, + tileWidthLast / TILE_ASPECT_PORTRAIT, + (containerHeight - (totalRowCount - 1) * gap) / totalRowCount, + ) - // Full-row tile that matches the common height - const fullRowAspect = tileWidthFull / commonHeight + // Target 16:9 for the last row if possible + const targetHeight = tileWidthLast / TILE_ASPECT_LANDSCAPE + const commonHeight = Math.min(maxPossibleHeight, targetHeight) - let tileHeightFull: number - let tileHeightLast: number - let fullRowAspectRatio: number - let lastRowAspectRatio: number - - if (fullRowAspect >= TILE_ASPECT_PORTRAIT) { - // Full-row aspect is within bounds — match heights cleanly - tileHeightFull = commonHeight - tileHeightLast = commonHeight - fullRowAspectRatio = fullRowAspect - lastRowAspectRatio = lastRowAspect - } else { - // Full rows would be too portrait — clamp to portrait bound - fullRowAspectRatio = TILE_ASPECT_PORTRAIT - tileHeightFull = tileWidthFull / fullRowAspectRatio - tileHeightLast = commonHeight - lastRowAspectRatio = lastRowAspect - } + const tileHeightFull = commonHeight + const tileHeightLast = commonHeight + const fullRowAspectRatio = tileWidthFull / commonHeight + const lastRowAspectRatio = tileWidthLast / commonHeight const totalHeight = fullRowCount * tileHeightFull + tileHeightLast + (totalRowCount - 1) * gap const totalWidth = Math.max( @@ -293,11 +286,16 @@ export const computeAdaptiveGrid = ( // unacceptable — heavily penalize. If tiles are below MIN_TILE_HEIGHT, // overflow is acceptable (tiles are already too small to shrink further). const minTileHeightInCandidate = Math.min(...candidate.rows.map(r => r.tileHeight)) - const overflowPenalty = + const verticalOverflowPenalty = verticalOverflow > 0 && minTileHeightInCandidate >= MIN_TILE_HEIGHT ? verticalOverflow * 1_000_000 : verticalOverflow + const horizontalOverflowPenalty = + horizontalOverflow > 0 && minTileHeightInCandidate >= MIN_TILE_HEIGHT + ? horizontalOverflow * 1_000_000 + : horizontalOverflow + // Min-tile-height penalty: even when fitting, discourage layouts // where tiles are below the minimum acceptable size. const minTileHeightPenalty = @@ -306,7 +304,8 @@ export const computeAdaptiveGrid = ( : 0 const score: LayoutScore = { - verticalOverflowPenalty: overflowPenalty, + verticalOverflowPenalty, + horizontalOverflowPenalty, minTileHeightPenalty, negativeArea: -totalArea, whitespace, diff --git a/src/app/components/VideoCallContent.svelte b/src/app/components/VideoCallContent.svelte index 0c392ca6..72a180e7 100644 --- a/src/app/components/VideoCallContent.svelte +++ b/src/app/components/VideoCallContent.svelte @@ -296,10 +296,13 @@ {#if tileGrid}
{#each tileGrid.rows as row, rowIndex} + {@const offset = tileGrid.rows + .slice(0, rowIndex) + .reduce((sum, r) => sum + r.columnCount, 0)}
- {#each videoTiles.slice(rowIndex * row.columnCount, rowIndex * row.columnCount + row.columnCount) as tile (tileKey(tile))} + {#each videoTiles.slice(offset, offset + row.columnCount) as tile (tileKey(tile))}