From 0cf5101931405a9810ea1849ce3bef80810ba5cf Mon Sep 17 00:00:00 2001 From: Carlos Santos <4a.santos@gmail.com> Date: Thu, 20 Nov 2025 16:56:03 +0100 Subject: [PATCH] ov-components: Optimize layout handling with caching and resize improvements --- .../lib/components/layout/layout.component.ts | 29 +- .../src/lib/models/layout.model.ts | 758 +++++++++++------- .../src/lib/services/layout/layout.service.ts | 6 +- 3 files changed, 482 insertions(+), 311 deletions(-) diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/layout/layout.component.ts b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/layout/layout.component.ts index 097dd9db8..ad5f7bb17 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/layout/layout.component.ts +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/layout/layout.component.ts @@ -104,8 +104,10 @@ export class LayoutComponent implements OnInit, OnDestroy, AfterViewInit { private destroy$ = new Subject(); private resizeObserver: ResizeObserver; private resizeTimeout: NodeJS.Timeout; + private rafId: number | null = null; private videoIsAtRight: boolean = false; private lastLayoutWidth: number = 0; + private readonly SIGNIFICANT_RESIZE_THRESHOLD = 5; // pixels /** * @ignore @@ -140,6 +142,10 @@ export class LayoutComponent implements OnInit, OnDestroy, AfterViewInit { this.destroy$.complete(); this.localParticipant = undefined; this.remoteParticipants = []; + if (this.rafId !== null) { + cancelAnimationFrame(this.rafId); + this.rafId = null; + } this.resizeObserver?.disconnect(); this.layoutService.clear(); } @@ -217,11 +223,22 @@ export class LayoutComponent implements OnInit, OnDestroy, AfterViewInit { private listenToResizeLayout() { this.resizeObserver = new ResizeObserver((entries) => { - clearTimeout(this.resizeTimeout); + // Cancel any pending animation frame to avoid duplicate work + if (this.rafId !== null) { + cancelAnimationFrame(this.rafId); + } + + // Use requestAnimationFrame for better performance + this.rafId = requestAnimationFrame(() => { + const { width: parentWidth } = entries[0].contentRect; + + // Only update if the change is significant (threshold-based) + if (Math.abs(this.lastLayoutWidth - parentWidth) < this.SIGNIFICANT_RESIZE_THRESHOLD) { + this.rafId = null; + return; + } - this.resizeTimeout = setTimeout(() => { if (this.localParticipant?.isMinimized) { - const { width: parentWidth } = entries[0].contentRect; if (this.panelService.isPanelOpened()) { if (this.lastLayoutWidth < parentWidth) { // Layout is bigger than before. Maybe the settings panel(wider) has been transitioned to another panel. @@ -240,9 +257,11 @@ export class LayoutComponent implements OnInit, OnDestroy, AfterViewInit { this.moveStreamToRight(parentWidth); } } - this.lastLayoutWidth = parentWidth; } - }, 100); + + this.lastLayoutWidth = parentWidth; + this.rafId = null; + }); }); this.resizeObserver.observe(this.layoutContainer.element.nativeElement); diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/models/layout.model.ts b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/models/layout.model.ts index c73bc6992..1dbbcfd76 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/models/layout.model.ts +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/models/layout.model.ts @@ -87,6 +87,88 @@ export interface ExtendedLayoutOptions extends OpenViduLayoutOptions { containerHeight: number; } +/** + * Strategy interface for layout area calculations + * @internal + */ +interface LayoutStrategy { + calculateAreas( + containerWidth: number, + containerHeight: number, + bigPercentage: number, + bigWidth: number, + bigHeight: number + ): { big: LayoutArea; small: LayoutArea; offsetLeft: number; offsetTop: number }; + + determineBigFirst(bigFirst: BigFirstOption): boolean; +} + +/** + * Tall layout strategy: arrange small elements at bottom + * @internal + */ +class TallLayoutStrategy implements LayoutStrategy { + calculateAreas( + containerWidth: number, + containerHeight: number, + bigPercentage: number, + bigWidth: number, + bigHeight: number + ): { big: LayoutArea; small: LayoutArea; offsetLeft: number; offsetTop: number } { + const offsetTop = bigHeight; + return { + big: { top: 0, left: 0, width: bigWidth, height: bigHeight }, + small: { + top: offsetTop, + left: 0, + width: containerWidth, + height: containerHeight - offsetTop + }, + offsetLeft: 0, + offsetTop + }; + } + + determineBigFirst(bigFirst: BigFirstOption): boolean { + if (bigFirst === 'column') return false; + if (bigFirst === 'row') return true; + return !!bigFirst; + } +} + +/** + * Wide layout strategy: arrange small elements on right + * @internal + */ +class WideLayoutStrategy implements LayoutStrategy { + calculateAreas( + containerWidth: number, + containerHeight: number, + bigPercentage: number, + bigWidth: number, + bigHeight: number + ): { big: LayoutArea; small: LayoutArea; offsetLeft: number; offsetTop: number } { + const offsetLeft = bigWidth; + return { + big: { top: 0, left: 0, width: bigWidth, height: bigHeight }, + small: { + top: 0, + left: offsetLeft, + width: containerWidth - offsetLeft, + height: containerHeight + }, + offsetLeft, + offsetTop: 0 + }; + } + + determineBigFirst(bigFirst: BigFirstOption): boolean { + if (bigFirst === 'column') return true; + if (bigFirst === 'row') return false; + return !!bigFirst; + } +} + /** * Layout configuration constants */ @@ -95,10 +177,10 @@ export const LAYOUT_CONSTANTS = { DEFAULT_VIDEO_HEIGHT: 480, DEFAULT_MAX_RATIO: 3 / 2, DEFAULT_MIN_RATIO: 9 / 16, - DEFAULT_BIG_PERCENTAGE: 0.8, + DEFAULT_BIG_PERCENTAGE: 0.85, UPDATE_TIMEOUT: 50, - ANIMATION_DURATION: '0.1s', - ANIMATION_EASING: 'linear' + ANIMATION_DURATION: '0.15s', + ANIMATION_EASING: 'ease-in-out' } as const; /** @@ -162,6 +244,8 @@ export class OpenViduLayout { private layoutContainer!: HTMLElement; private opts!: OpenViduLayoutOptions; private dimensionsCache = new Map(); + private layoutCache = new Map(); + private readonly CACHE_SIZE_LIMIT = 100; /** * Update the layout container @@ -253,6 +337,31 @@ export class OpenViduLayout { */ clearCache(): void { this.dimensionsCache.clear(); + this.layoutCache.clear(); + } + + /** + * Manage cache size to prevent unlimited growth + * @hidden + */ + private manageCacheSize(cache: Map): void { + if (cache.size > this.CACHE_SIZE_LIMIT) { + const firstKey = cache.keys().next().value; + if (firstKey !== undefined) { + cache.delete(firstKey); + } + } + } + + /** + * Generate cache key for layout calculations + * @hidden + */ + private getLayoutCacheKey(opts: ExtendedLayoutOptions, elements: ElementDimensions[]): string { + const elementKey = elements + .map(e => `${e.width}x${e.height}x${e.big ? '1' : '0'}`) + .join('_'); + return `${opts.containerWidth}x${opts.containerHeight}_${elementKey}_${opts.bigPercentage}`; } @@ -330,11 +439,17 @@ export class OpenViduLayout { if (animate) { setTimeout(() => { // animation added in css transition: all .1s linear; - this.animateElement(elem, targetPosition); + elem.style.transition = `all ${LAYOUT_CONSTANTS.ANIMATION_DURATION} ${LAYOUT_CONSTANTS.ANIMATION_EASING}`; + Object.entries(targetPosition).forEach(([key, value]) => { + (elem.style as any)[key] = value; + }); this.fixAspectRatio(elem, width); }, 10); } else { - this.setElementPosition(elem, targetPosition); + elem.style.transition = 'none'; + Object.entries(targetPosition).forEach(([key, value]) => { + (elem.style as any)[key] = value; + }); if (!elem.classList.contains(LayoutClass.CLASS_NAME)) { elem.classList.add(LayoutClass.CLASS_NAME); } @@ -342,17 +457,6 @@ export class OpenViduLayout { this.fixAspectRatio(elem, width); } - private setElementPosition(elem: HTMLVideoElement, targetPosition: { [key: string]: string }) { - Object.keys(targetPosition).forEach((key) => { - (elem.style as any)[key] = targetPosition[key]; - }); - } - - private animateElement(elem: HTMLVideoElement, targetPosition: { [key: string]: string }) { - elem.style.transition = `all ${LAYOUT_CONSTANTS.ANIMATION_DURATION} ${LAYOUT_CONSTANTS.ANIMATION_EASING}`; - this.setElementPosition(elem, targetPosition); - } - /** * @hidden */ @@ -447,6 +551,9 @@ export class OpenViduLayout { if (cached) { return cached; } + + // Manage cache size before adding new entry + this.manageCacheSize(this.dimensionsCache); let bestArea = 0; let bestCols = 1; let bestRows = 1; @@ -511,7 +618,173 @@ export class OpenViduLayout { private getVideoRatio(element: ElementDimensions): number { return element.height / element.width; } + + /** + * Calculate big element dimensions with minimum percentage constraints + * @hidden + */ + private calculateBigDimensions( + bigOnes: ElementDimensions[], + bigWidth: number, + bigHeight: number, + bigFixedRatio: boolean, + bigMinRatio: number, + bigMaxRatio: number, + bigMaxWidth: number, + bigMaxHeight: number, + minBigPercentage: number, + containerWidth: number, + containerHeight: number, + minRatio: number, + maxRatio: number, + smallOnes: ElementDimensions[], + smallMaxWidth: number, + smallMaxHeight: number, + isTall: boolean + ): number { + if (minBigPercentage <= 0) { + return isTall ? bigHeight : bigWidth; + } + + // Find the best size for the big area + const bigDimensions = !bigFixedRatio + ? this.getBestDimensions(bigMinRatio, bigMaxRatio, bigWidth, bigHeight, bigOnes.length, bigMaxWidth, bigMaxHeight) + : this.getBestDimensions( + bigOnes[0].height / bigOnes[0].width, + bigOnes[0].height / bigOnes[0].width, + bigWidth, + bigHeight, + bigOnes.length, + bigMaxWidth, + bigMaxHeight + ); + + const minSize = isTall ? containerHeight * minBigPercentage : containerWidth * minBigPercentage; + const calculatedSize = isTall + ? bigDimensions.targetHeight * bigDimensions.targetRows + : bigDimensions.targetWidth * bigDimensions.targetCols; + let adjustedSize = Math.max(minSize, Math.min(isTall ? bigHeight : bigWidth, calculatedSize)); + + // Don't awkwardly scale the small area bigger than we need to + const smallDimensions = isTall + ? this.getBestDimensions(minRatio, maxRatio, containerWidth, containerHeight - adjustedSize, smallOnes.length, smallMaxWidth, smallMaxHeight) + : this.getBestDimensions(minRatio, maxRatio, containerWidth - adjustedSize, containerHeight, smallOnes.length, smallMaxWidth, smallMaxHeight); + + const smallCalculatedSize = isTall + ? smallDimensions.targetRows * smallDimensions.targetHeight + : smallDimensions.targetCols * smallDimensions.targetWidth; + + adjustedSize = Math.max(adjustedSize, (isTall ? containerHeight : containerWidth) - smallCalculatedSize); + + return adjustedSize; + } + + /** + * Get layout strategy based on container and video ratios + * @hidden + */ + private getLayoutStrategy(availableRatio: number, videoRatio: number): LayoutStrategy { + return availableRatio > videoRatio ? new TallLayoutStrategy() : new WideLayoutStrategy(); + } + + /** + * Calculate layout areas for big and small elements using Strategy Pattern + * @hidden + */ + private calculateLayoutAreas( + availableRatio: number, + videoRatio: number, + containerWidth: number, + containerHeight: number, + bigPercentage: number, + bigFirst: BigFirstOption, + bigOnes: ElementDimensions[], + smallOnes: ElementDimensions[], + opts: ExtendedLayoutOptions + ): { big: LayoutArea | null; small: LayoutArea | null; bigFirst: boolean } { + const strategy = this.getLayoutStrategy(availableRatio, videoRatio); + const isTall = availableRatio > videoRatio; + + // Calculate initial big dimensions + const initialBigWidth = isTall ? containerWidth : Math.floor(containerWidth * bigPercentage); + const initialBigHeight = isTall ? Math.floor(containerHeight * bigPercentage) : containerHeight; + + // Calculate final big dimensions with constraints + const bigWidth = isTall ? initialBigWidth : this.calculateBigDimensions( + bigOnes, + initialBigWidth, + initialBigHeight, + opts.bigFixedRatio, + opts.bigMinRatio, + opts.bigMaxRatio, + opts.bigMaxWidth, + opts.bigMaxHeight, + opts.minBigPercentage, + containerWidth, + containerHeight, + opts.minRatio, + opts.maxRatio, + smallOnes, + opts.smallMaxWidth, + opts.smallMaxHeight, + false + ); + + const bigHeight = isTall ? this.calculateBigDimensions( + bigOnes, + initialBigWidth, + initialBigHeight, + opts.bigFixedRatio, + opts.bigMinRatio, + opts.bigMaxRatio, + opts.bigMaxWidth, + opts.bigMaxHeight, + opts.minBigPercentage, + containerWidth, + containerHeight, + opts.minRatio, + opts.maxRatio, + smallOnes, + opts.smallMaxWidth, + opts.smallMaxHeight, + true + ) : initialBigHeight; + + // Use strategy to calculate areas + const { big, small, offsetLeft, offsetTop } = strategy.calculateAreas( + containerWidth, + containerHeight, + bigPercentage, + bigWidth, + bigHeight + ); + + const showBigFirst = strategy.determineBigFirst(bigFirst); + + if (showBigFirst) { + return { big, small, bigFirst: true }; + } else { + // Swap positions for bigFirst=false + const bigOffsetLeft = containerWidth - offsetLeft; + const bigOffsetTop = containerHeight - offsetTop; + return { + big: { left: bigOffsetLeft, top: bigOffsetTop, width: bigWidth, height: bigHeight }, + small: { top: 0, left: 0, width: containerWidth - offsetLeft, height: containerHeight - offsetTop }, + bigFirst: false + }; + } + } private getLayout(opts: ExtendedLayoutOptions, elements: ElementDimensions[]) { + // Check cache first + const cacheKey = this.getLayoutCacheKey(opts, elements); + const cached = this.layoutCache.get(cacheKey); + if (cached) { + return cached; + } + + // Manage cache size before adding new entry + this.manageCacheSize(this.layoutCache); + const { maxRatio = LAYOUT_CONSTANTS.DEFAULT_MAX_RATIO, minRatio = LAYOUT_CONSTANTS.DEFAULT_MIN_RATIO, @@ -536,17 +809,8 @@ export class OpenViduLayout { scaleLastRow = true, bigScaleLastRow = true } = opts; - const availableRatio = containerHeight / containerWidth; - let offsetLeft = 0; - let offsetTop = 0; - let bigOffsetTop = 0; - let bigOffsetLeft = 0; + // Separate big and small elements const bigIndices: number[] = []; - let bigBoxes: LayoutBox[] = []; - let smallBoxes: LayoutBox[] = []; - let areas: { big: LayoutArea | null; small: LayoutArea | null } = { big: null, small: null }; - - // Move to Get Layout const smallOnes = elements.filter((element) => !element.big); const bigOnes = elements.filter((element, idx) => { if (element.big) { @@ -555,176 +819,38 @@ export class OpenViduLayout { } return false; }); - //TODO: Habia un codigo personalizado que servía para - //TODO: tener videos grandes, pequeños y normales - //.filter((x) => !smallOnes.includes(x)); - // const normalOnes: HTMLVideoElement[] = Array.prototype.filter - // .call( - // this.layoutContainer.querySelectorAll( - // `#${id}>*:not(.${this.opts.bigClass})` - // ), - // () => this.filterDisplayNone - // ) - // .filter((x) => !smallOnes.includes(x)); - // this.attachElements(bigOnes, normalOnes, smallOnes); + // Determine layout areas based on element distribution + let areas: { big: LayoutArea | null; small: LayoutArea | null }; + let bigBoxes: LayoutBox[] = []; + let smallBoxes: LayoutBox[] = []; + if (bigOnes.length > 0 && smallOnes.length > 0) { - let bigWidth; - let bigHeight; - let showBigFirst = bigFirst; - - if (availableRatio > this.getVideoRatio(bigOnes[0])) { - // We are tall, going to take up the whole width and arrange small - // guys at the bottom - bigWidth = containerWidth; - bigHeight = Math.floor(containerHeight * bigPercentage); - if (minBigPercentage > 0) { - // Find the best size for the big area - let bigDimensions; - if (!bigFixedRatio) { - bigDimensions = this.getBestDimensions( - bigMinRatio, - bigMaxRatio, - bigWidth, - bigHeight, - bigOnes.length, - bigMaxWidth, - bigMaxHeight - ); - } else { - // Use the ratio of the first video element we find to approximate - const ratio = bigOnes[0].height / bigOnes[0].width; - bigDimensions = this.getBestDimensions( - ratio, - ratio, - bigWidth, - bigHeight, - bigOnes.length, - bigMaxWidth, - bigMaxHeight - ); - } - bigHeight = Math.max( - containerHeight * minBigPercentage, - Math.min(bigHeight, bigDimensions.targetHeight * bigDimensions.targetRows) - ); - // Don't awkwardly scale the small area bigger than we need to and end up with floating - // videos in the middle - const smallDimensions = this.getBestDimensions( - minRatio, - maxRatio, - containerWidth, - containerHeight - bigHeight, - smallOnes.length, - smallMaxWidth, - smallMaxHeight - ); - bigHeight = Math.max(bigHeight, containerHeight - smallDimensions.targetRows * smallDimensions.targetHeight); - } - offsetTop = bigHeight; - bigOffsetTop = containerHeight - offsetTop; - if (bigFirst === 'column') { - showBigFirst = false; - } else if (bigFirst === 'row') { - showBigFirst = true; - } - } else { - // We are wide, going to take up the whole height and arrange the small - // guys on the right - bigHeight = containerHeight; - bigWidth = Math.floor(containerWidth * bigPercentage); - if (minBigPercentage > 0) { - // Find the best size for the big area - let bigDimensions; - if (!bigFixedRatio) { - bigDimensions = this.getBestDimensions( - bigMinRatio, - bigMaxRatio, - bigWidth, - bigHeight, - bigOnes.length, - bigMaxWidth, - bigMaxHeight - ); - } else { - // Use the ratio of the first video element we find to approximate - const ratio = bigOnes[0].height / bigOnes[0].width; - bigDimensions = this.getBestDimensions( - ratio, - ratio, - bigWidth, - bigHeight, - bigOnes.length, - bigMaxWidth, - bigMaxHeight - ); - } - bigWidth = Math.max( - containerWidth * minBigPercentage, - Math.min(bigWidth, bigDimensions.targetWidth * bigDimensions.targetCols) - ); - // Don't awkwardly scale the small area bigger than we need to and end up with floating - // videos in the middle - const smallDimensions = this.getBestDimensions( - minRatio, - maxRatio, - containerWidth - bigWidth, - containerHeight, - smallOnes.length, - smallMaxWidth, - smallMaxHeight - ); - bigWidth = Math.max(bigWidth, containerWidth - smallDimensions.targetCols * smallDimensions.targetWidth); - } - offsetLeft = bigWidth; - bigOffsetLeft = containerWidth - offsetLeft; - if (bigFirst === 'column') { - showBigFirst = true; - } else if (bigFirst === 'row') { - showBigFirst = false; - } - } - if (showBigFirst) { - areas.big = { - top: 0, - left: 0, - width: bigWidth, - height: bigHeight - }; - areas.small = { - top: offsetTop, - left: offsetLeft, - width: containerWidth - offsetLeft, - height: containerHeight - offsetTop - }; - } else { - areas.big = { - left: bigOffsetLeft, - top: bigOffsetTop, - width: bigWidth, - height: bigHeight - }; - areas.small = { - top: 0, - left: 0, - width: containerWidth - offsetLeft, - height: containerHeight - offsetTop - }; - } - } else if (bigOnes.length > 0 && smallOnes.length === 0) { - // We only have one bigOne just center it - areas.big = { - top: 0, - left: 0, - width: containerWidth, - height: containerHeight + // Mixed layout: calculate areas for both big and small elements + const availableRatio = containerHeight / containerWidth; + const layoutAreas = this.calculateLayoutAreas( + availableRatio, + this.getVideoRatio(bigOnes[0]), + containerWidth, + containerHeight, + bigPercentage, + bigFirst, + bigOnes, + smallOnes, + opts + ); + areas = { big: layoutAreas.big, small: layoutAreas.small }; + } else if (bigOnes.length > 0) { + // Only big elements: use full container + areas = { + big: { top: 0, left: 0, width: containerWidth, height: containerHeight }, + small: null }; } else { - areas.small = { - top: offsetTop, - left: offsetLeft, - width: containerWidth - offsetLeft, - height: containerHeight - offsetTop + // Only small elements: use full container + areas = { + big: null, + small: { top: 0, left: 0, width: containerWidth, height: containerHeight } }; } @@ -778,7 +904,123 @@ export class OpenViduLayout { smallBoxesIdx += 1; } }); - return { boxes, areas }; + + const result = { boxes, areas }; + // Cache the result for future use + this.layoutCache.set(cacheKey, result); + return result; + } + + /** + * Build layout rows from element ratios + * @hidden + */ + private buildLayoutRows( + ratios: number[], + dimensions: BestDimensions, + fixedRatio: boolean, + containerWidth: number, + maxHeight: number + ): { rows: LayoutRow[]; totalRowHeight: number } { + const rows: LayoutRow[] = []; + let row: LayoutRow | undefined; + + // Create rows and calculate their dimensions + for (let i = 0; i < ratios.length; i++) { + if (i % dimensions.targetCols === 0) { + row = { ratios: [], width: 0, height: 0 }; + rows.push(row); + } + + if (row) { + const ratio = ratios[i]; + row.ratios.push(ratio); + const targetWidth = fixedRatio ? dimensions.targetHeight / ratio : dimensions.targetWidth; + row.width += targetWidth; + row.height = dimensions.targetHeight; + } + } + + // Adjust rows that exceed container width + let totalRowHeight = 0; + for (const r of rows) { + if (r.width > containerWidth) { + r.height = Math.floor(r.height * (containerWidth / r.width)); + r.width = containerWidth; + } + totalRowHeight += r.height; + } + + return { rows, totalRowHeight }; + } + + /** + * Scale rows to fill container height if needed + * @hidden + */ + private scaleRowsToFit( + rows: LayoutRow[], + totalRowHeight: number, + containerWidth: number, + containerHeight: number, + maxHeight: number + ): number { + let remainingShortRows = rows.filter(r => r.width < containerWidth && r.height < maxHeight).length; + if (remainingShortRows === 0) return totalRowHeight; + + let remainingHeightDiff = containerHeight - totalRowHeight; + let adjustedTotalHeight = 0; + + for (const row of rows) { + if (row.width < containerWidth && remainingShortRows > 0) { + let extraHeight = remainingHeightDiff / remainingShortRows; + const maxExtraHeight = Math.floor(((containerWidth - row.width) / row.width) * row.height); + + if (extraHeight > maxExtraHeight) { + extraHeight = maxExtraHeight; + } + + row.width += Math.floor((extraHeight / row.height) * row.width); + row.height += extraHeight; + remainingHeightDiff -= extraHeight; + remainingShortRows -= 1; + } + adjustedTotalHeight += row.height; + } + + return adjustedTotalHeight; + } + + /** + * Calculate vertical offset based on alignment + * @hidden + */ + private calculateVerticalOffset(alignItems: LayoutAlignment, containerHeight: number, totalRowHeight: number): number { + switch (alignItems) { + case LayoutAlignment.START: + return 0; + case LayoutAlignment.END: + return containerHeight - totalRowHeight; + case LayoutAlignment.CENTER: + default: + return (containerHeight - totalRowHeight) / 2; + } + } + + /** + * Calculate horizontal offset based on alignment + * @hidden + */ + private calculateHorizontalOffset(alignItems: LayoutAlignment, containerWidth: number, rowWidth: number): number { + switch (alignItems) { + case LayoutAlignment.START: + return 0; + case LayoutAlignment.END: + return containerWidth - rowWidth; + case LayoutAlignment.CENTER: + default: + return (containerWidth - rowWidth) / 2; + } } private getLayoutAux(opts: Partial, elements: ElementDimensions[]): LayoutBox[] { @@ -798,125 +1040,33 @@ export class OpenViduLayout { const ratios = elements.map((element) => element.height / element.width); const count = ratios.length; - let dimensions; + // Calculate target dimensions for elements + const targetRatio = fixedRatio && ratios.length > 0 ? ratios[0] : null; + const dimensions = targetRatio + ? this.getBestDimensions(targetRatio, targetRatio, containerWidth, containerHeight, count, maxWidth, maxHeight) + : this.getBestDimensions(minRatio, maxRatio, containerWidth, containerHeight, count, maxWidth, maxHeight); - if (!fixedRatio) { - dimensions = this.getBestDimensions(minRatio, maxRatio, containerWidth, containerHeight, count, maxWidth, maxHeight); - } else { - // Use the ratio of the first video element we find to approximate - const ratio = ratios.length > 0 ? ratios[0] : LAYOUT_CONSTANTS.DEFAULT_MIN_RATIO; - dimensions = this.getBestDimensions(ratio, ratio, containerWidth, containerHeight, count, maxWidth, maxHeight); - } + // Build and adjust rows + const { rows, totalRowHeight } = this.buildLayoutRows(ratios, dimensions, fixedRatio, containerWidth, maxHeight); + const finalRowHeight = scaleLastRow && totalRowHeight < containerHeight + ? this.scaleRowsToFit(rows, totalRowHeight, containerWidth, containerHeight, maxHeight) + : totalRowHeight; - // Loop through each stream in the container and place it inside - let x = 0; - let y = 0; - const rows: LayoutRow[] = []; - let row: LayoutRow | undefined; + // Calculate starting position + let y = this.calculateVerticalOffset(alignItems, containerHeight, finalRowHeight); + + // Position elements in rows const boxes: LayoutBox[] = []; + for (const row of rows) { + let x = this.calculateHorizontalOffset(alignItems, containerWidth, row.width); - // Iterate through the children and create an array with a new item for each row - // and calculate the width of each row so that we know if we go over the size and need - // to adjust - for (let i = 0; i < ratios.length; i++) { - if (i % dimensions.targetCols === 0) { - // This is a new row - row = { - ratios: [], - width: 0, - height: 0 - }; - rows.push(row); - } - const ratio = ratios[i]; - if (row) { - row.ratios.push(ratio); + for (const ratio of row.ratios) { let targetWidth = dimensions.targetWidth; - const targetHeight = dimensions.targetHeight; - // If we're using a fixedRatio then we need to set the correct ratio for this element - if (fixedRatio) { - targetWidth = targetHeight / ratio; - } - row.width += targetWidth; - row.height = targetHeight; - } - } - // Calculate total row height adjusting if we go too wide - let totalRowHeight = 0; - let remainingShortRows = 0; - for (let i = 0; i < rows.length; i++) { - row = rows[i]; - if (row.width > containerWidth) { - // Went over on the width, need to adjust the height proportionally - row.height = Math.floor(row.height * (containerWidth / row.width)); - row.width = containerWidth; - } else if (row.width < containerWidth && row.height < maxHeight) { - remainingShortRows += 1; - } - totalRowHeight += row.height; - } - if (scaleLastRow && totalRowHeight < containerHeight && remainingShortRows > 0) { - // We can grow some of the rows, we're not taking up the whole height - let remainingHeightDiff = containerHeight - totalRowHeight; - totalRowHeight = 0; - for (let i = 0; i < rows.length; i++) { - row = rows[i]; - if (row.width < containerWidth) { - // Evenly distribute the extra height between the short rows - let extraHeight = remainingHeightDiff / remainingShortRows; - if (extraHeight / row.height > (containerWidth - row.width) / row.width) { - // We can't go that big or we'll go too wide - extraHeight = Math.floor(((containerWidth - row.width) / row.width) * row.height); - } - row.width += Math.floor((extraHeight / row.height) * row.width); - row.height += extraHeight; - remainingHeightDiff -= extraHeight; - remainingShortRows -= 1; - } - totalRowHeight += row.height; - } - } - // vertical centering - switch (alignItems) { - case 'start': - y = 0; - break; - case 'end': - y = containerHeight - totalRowHeight; - break; - case 'center': - default: - y = (containerHeight - totalRowHeight) / 2; - break; - } - // Iterate through each row and place each child - for (let i = 0; i < rows.length; i++) { - row = rows[i]; - let rowMarginLeft; - switch (alignItems) { - case 'start': - rowMarginLeft = 0; - break; - case 'end': - rowMarginLeft = containerWidth - row.width; - break; - case 'center': - default: - rowMarginLeft = (containerWidth - row.width) / 2; - break; - } - x = rowMarginLeft; - let targetHeight = row.height; - for (let j = 0; j < row.ratios.length; j++) { - const ratio = row.ratios[j]; + const targetHeight = row.height; - let targetWidth = dimensions.targetWidth; - targetHeight = row.height; - // If we're using a fixedRatio then we need to set the correct ratio for this element if (fixedRatio) { targetWidth = Math.floor(targetHeight / ratio); } else if (targetHeight / targetWidth !== dimensions.targetHeight / dimensions.targetWidth) { - // We grew this row, we need to adjust the width to account for the increase in height targetWidth = Math.floor((dimensions.targetWidth / dimensions.targetHeight) * targetHeight); } @@ -928,7 +1078,7 @@ export class OpenViduLayout { }); x += targetWidth; } - y += targetHeight; + y += row.height; } return boxes; } diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/services/layout/layout.service.ts b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/services/layout/layout.service.ts index 862dbad09..614fde139 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/services/layout/layout.service.ts +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/services/layout/layout.service.ts @@ -80,6 +80,7 @@ export class LayoutService { protected getOptions(): OpenViduLayoutOptions { const ratios = this.getResponsiveRatios(); const percentages = this.getResponsivePercentages(); + const isMobile = this.viewportSrv.isMobile(); return { maxRatio: ratios.maxRatio, @@ -94,7 +95,8 @@ export class LayoutService { bigMaxRatio: ratios.bigMaxRatio, bigMinRatio: ratios.bigMinRatio, bigFirst: true, - animate: true, + // Disable animations on mobile for better performance + animate: !isMobile, alignItems: LayoutAlignment.CENTER, bigAlignItems: LayoutAlignment.CENTER, smallAlignItems: LayoutAlignment.CENTER, @@ -105,7 +107,7 @@ export class LayoutService { bigMaxWidth: Infinity, bigMaxHeight: Infinity, scaleLastRow: true, - bigScaleLastRow: true + bigScaleLastRow: false }; }