mirror of https://github.com/OpenVidu/openvidu.git
ov-component: introduce layout management classes and caching mechanism
- Added LayoutDimensionsCache for caching dimension calculations to optimize layout rendering. - Implemented LayoutRenderer for handling DOM manipulation and rendering of layout elements. - Created layout-types model to define various layout-related types and constants. - Developed OpenViduLayout class to orchestrate layout calculations and rendering, maintaining backward compatibility. - Updated document and layout services to reference new layout model structure.master
parent
3c02121ebe
commit
961867941a
|
|
@ -14262,6 +14262,7 @@
|
||||||
"integrity": "sha512-SL0JY3DaxylDuo/MecFeiC+7pedM0zia33zl0vcjgwcq1q1FWWF1To9EIauPbl8GbMCU0R2e0uJ8bZunhYKD2g==",
|
"integrity": "sha512-SL0JY3DaxylDuo/MecFeiC+7pedM0zia33zl0vcjgwcq1q1FWWF1To9EIauPbl8GbMCU0R2e0uJ8bZunhYKD2g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cli-truncate": "^4.0.0",
|
"cli-truncate": "^4.0.0",
|
||||||
"colorette": "^2.0.20",
|
"colorette": "^2.0.20",
|
||||||
|
|
@ -21040,6 +21041,7 @@
|
||||||
"integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==",
|
"integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
|
|
@ -21368,6 +21370,7 @@
|
||||||
"integrity": "sha512-QcQ72gh8a+7JO63TAx/6XZf/CWhgMzu5m0QirvPfGvptOusAxG12w2+aua1Jkjr7hzaWDnJ2n6JFeexMHI+Zjg==",
|
"integrity": "sha512-QcQ72gh8a+7JO63TAx/6XZf/CWhgMzu5m0QirvPfGvptOusAxG12w2+aua1Jkjr7hzaWDnJ2n6JFeexMHI+Zjg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/bonjour": "^3.5.13",
|
"@types/bonjour": "^3.5.13",
|
||||||
"@types/connect-history-api-fallback": "^1.5.4",
|
"@types/connect-history-api-fallback": "^1.5.4",
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ import { animate, style, transition, trigger } from '@angular/animations';
|
||||||
import { MatDrawerContainer, MatSidenav } from '@angular/material/sidenav';
|
import { MatDrawerContainer, MatSidenav } from '@angular/material/sidenav';
|
||||||
import { skip, Subject, takeUntil } from 'rxjs';
|
import { skip, Subject, takeUntil } from 'rxjs';
|
||||||
import { DataTopic } from '../../models/data-topic.model';
|
import { DataTopic } from '../../models/data-topic.model';
|
||||||
import { SidenavMode } from '../../models/layout.model';
|
import { SidenavMode } from '../../models/layout/layout.model';
|
||||||
import { ILogger } from '../../models/logger.model';
|
import { ILogger } from '../../models/logger.model';
|
||||||
import { PanelStatusInfo, PanelType } from '../../models/panel.model';
|
import { PanelStatusInfo, PanelType } from '../../models/panel.model';
|
||||||
import { RoomStatusData } from '../../models/room.model';
|
import { RoomStatusData } from '../../models/room.model';
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,667 @@
|
||||||
|
import { LayoutDimensionsCache } from './layout-dimensions-cache.model';
|
||||||
|
import {
|
||||||
|
BestDimensions,
|
||||||
|
CategorizedElements,
|
||||||
|
ElementDimensions,
|
||||||
|
ExtendedLayoutOptions,
|
||||||
|
LAYOUT_CONSTANTS,
|
||||||
|
LayoutAlignment,
|
||||||
|
LayoutBox,
|
||||||
|
LayoutCalculationResult,
|
||||||
|
LayoutRow
|
||||||
|
} from './layout-types.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pure calculation logic for layout positioning.
|
||||||
|
* Contains all mathematical algorithms for element positioning without DOM manipulation.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export class LayoutCalculator {
|
||||||
|
constructor(private dimensionsCache: LayoutDimensionsCache) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate complete layout including boxes and areas
|
||||||
|
* @param opts Extended layout options with container dimensions
|
||||||
|
* @param elements Array of element dimensions
|
||||||
|
* @returns Layout calculation result with boxes and areas
|
||||||
|
*/
|
||||||
|
calculateLayout(opts: ExtendedLayoutOptions, elements: ElementDimensions[]): LayoutCalculationResult {
|
||||||
|
const {
|
||||||
|
maxRatio = LAYOUT_CONSTANTS.DEFAULT_MAX_RATIO,
|
||||||
|
minRatio = LAYOUT_CONSTANTS.DEFAULT_MIN_RATIO,
|
||||||
|
fixedRatio = false,
|
||||||
|
bigPercentage = LAYOUT_CONSTANTS.DEFAULT_BIG_PERCENTAGE,
|
||||||
|
minBigPercentage = 0,
|
||||||
|
bigFixedRatio = false,
|
||||||
|
bigMaxRatio = LAYOUT_CONSTANTS.DEFAULT_MAX_RATIO,
|
||||||
|
bigMinRatio = LAYOUT_CONSTANTS.DEFAULT_MIN_RATIO,
|
||||||
|
bigFirst = true,
|
||||||
|
containerWidth = LAYOUT_CONSTANTS.DEFAULT_VIDEO_WIDTH,
|
||||||
|
containerHeight = LAYOUT_CONSTANTS.DEFAULT_VIDEO_HEIGHT,
|
||||||
|
alignItems = LayoutAlignment.CENTER,
|
||||||
|
bigAlignItems = LayoutAlignment.CENTER,
|
||||||
|
smallAlignItems = LayoutAlignment.CENTER,
|
||||||
|
maxWidth = Infinity,
|
||||||
|
maxHeight = Infinity,
|
||||||
|
smallMaxWidth = Infinity,
|
||||||
|
smallMaxHeight = Infinity,
|
||||||
|
bigMaxWidth = Infinity,
|
||||||
|
bigMaxHeight = Infinity,
|
||||||
|
scaleLastRow = true,
|
||||||
|
bigScaleLastRow = true
|
||||||
|
} = opts;
|
||||||
|
|
||||||
|
const availableRatio = containerHeight / containerWidth;
|
||||||
|
let offsetLeft = 0;
|
||||||
|
let offsetTop = 0;
|
||||||
|
let bigOffsetTop = 0;
|
||||||
|
let bigOffsetLeft = 0;
|
||||||
|
|
||||||
|
// Categorize elements
|
||||||
|
const categorized = this.categorizeElements(elements);
|
||||||
|
const { bigOnes, normalOnes, smallOnes, topBarOnes } = categorized;
|
||||||
|
|
||||||
|
let bigBoxes: LayoutBox[] = [];
|
||||||
|
let smallBoxes: LayoutBox[] = [];
|
||||||
|
let topBarBoxes: LayoutBox[] = [];
|
||||||
|
let normalBoxes: LayoutBox[] = [];
|
||||||
|
let areas: LayoutCalculationResult['areas'] = { big: null, normal: null, small: null, topBar: null };
|
||||||
|
|
||||||
|
// Handle different layout scenarios based on element types
|
||||||
|
if (bigOnes.length > 0 && (normalOnes.length > 0 || smallOnes.length > 0 || topBarOnes.length > 0)) {
|
||||||
|
// Scenario: Big elements with normal/small/topbar elements
|
||||||
|
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,
|
||||||
|
normalOnes.length + smallOnes.length + topBarOnes.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,
|
||||||
|
normalOnes.length + smallOnes.length + topBarOnes.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.normal = { top: offsetTop, left: offsetLeft, width: containerWidth - offsetLeft, height: containerHeight - offsetTop };
|
||||||
|
} else {
|
||||||
|
areas.big = { left: bigOffsetLeft, top: bigOffsetTop, width: bigWidth, height: bigHeight };
|
||||||
|
areas.normal = { top: 0, left: 0, width: containerWidth - offsetLeft, height: containerHeight - offsetTop };
|
||||||
|
}
|
||||||
|
} else if (bigOnes.length > 0 && normalOnes.length === 0 && smallOnes.length === 0 && topBarOnes.length === 0) {
|
||||||
|
// We only have bigOnes just center it
|
||||||
|
areas.big = { top: 0, left: 0, width: containerWidth, height: containerHeight };
|
||||||
|
} else if (normalOnes.length > 0 || smallOnes.length > 0 || topBarOnes.length > 0) {
|
||||||
|
// Only normal, small, and/or topbar elements
|
||||||
|
areas.normal = { top: offsetTop, left: offsetLeft, width: containerWidth - offsetLeft, height: containerHeight - offsetTop };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate boxes for each area
|
||||||
|
if (areas.big) {
|
||||||
|
bigBoxes = this.calculateBoxesForArea(
|
||||||
|
{
|
||||||
|
containerWidth: areas.big.width,
|
||||||
|
containerHeight: areas.big.height,
|
||||||
|
offsetLeft: areas.big.left,
|
||||||
|
offsetTop: areas.big.top,
|
||||||
|
fixedRatio: bigFixedRatio,
|
||||||
|
minRatio: bigMinRatio,
|
||||||
|
maxRatio: bigMaxRatio,
|
||||||
|
alignItems: bigAlignItems,
|
||||||
|
maxWidth: bigMaxWidth,
|
||||||
|
maxHeight: bigMaxHeight,
|
||||||
|
scaleLastRow: bigScaleLastRow
|
||||||
|
},
|
||||||
|
bigOnes
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (areas.normal) {
|
||||||
|
let currentTop = areas.normal.top;
|
||||||
|
let remainingHeight = areas.normal.height;
|
||||||
|
|
||||||
|
// 1. Position TopBar Elements at the very top (header style: full width, 80px height)
|
||||||
|
if (topBarOnes.length > 0) {
|
||||||
|
const topBarHeight = 80;
|
||||||
|
const topBarWidth = Math.floor(containerWidth / topBarOnes.length);
|
||||||
|
|
||||||
|
topBarBoxes = topBarOnes.map((element, idx) => {
|
||||||
|
return {
|
||||||
|
left: areas.normal!.left + idx * topBarWidth,
|
||||||
|
top: currentTop,
|
||||||
|
width: topBarWidth,
|
||||||
|
height: topBarHeight
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
currentTop += topBarHeight;
|
||||||
|
remainingHeight -= topBarHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Position Small Elements (reduced format)
|
||||||
|
if (smallOnes.length > 0) {
|
||||||
|
const maxSmallWidthAvailable = smallMaxWidth;
|
||||||
|
const maxSmallHeightAvailable = smallMaxHeight;
|
||||||
|
|
||||||
|
const tentativeCols = maxSmallWidthAvailable === Infinity
|
||||||
|
? smallOnes.length
|
||||||
|
: Math.max(1, Math.floor(containerWidth / maxSmallWidthAvailable));
|
||||||
|
const displayCols = Math.max(1, Math.min(smallOnes.length, tentativeCols));
|
||||||
|
|
||||||
|
const computedWidth = maxSmallWidthAvailable === Infinity
|
||||||
|
? Math.floor(containerWidth / displayCols)
|
||||||
|
: maxSmallWidthAvailable;
|
||||||
|
const computedHeight = maxSmallHeightAvailable === Infinity ? computedWidth : maxSmallHeightAvailable;
|
||||||
|
|
||||||
|
const rowWidth = displayCols * computedWidth;
|
||||||
|
const rowOffset = Math.floor(Math.max(0, containerWidth - rowWidth) / 2);
|
||||||
|
|
||||||
|
smallBoxes = smallOnes.map((element, idx) => {
|
||||||
|
const col = idx % displayCols;
|
||||||
|
return {
|
||||||
|
left: areas.normal!.left + col * computedWidth + rowOffset,
|
||||||
|
top: currentTop,
|
||||||
|
width: computedWidth,
|
||||||
|
height: computedHeight
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
currentTop += computedHeight;
|
||||||
|
remainingHeight -= computedHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Position Normal Elements in remaining space
|
||||||
|
if (normalOnes.length > 0) {
|
||||||
|
normalBoxes = this.calculateBoxesForArea(
|
||||||
|
{
|
||||||
|
containerWidth: areas.normal.width,
|
||||||
|
containerHeight: Math.max(0, remainingHeight),
|
||||||
|
offsetLeft: areas.normal.left,
|
||||||
|
offsetTop: currentTop,
|
||||||
|
fixedRatio,
|
||||||
|
minRatio,
|
||||||
|
maxRatio,
|
||||||
|
alignItems: areas.big ? smallAlignItems : alignItems,
|
||||||
|
maxWidth: areas.big ? maxWidth : maxWidth,
|
||||||
|
maxHeight: areas.big ? maxHeight : maxHeight,
|
||||||
|
scaleLastRow
|
||||||
|
},
|
||||||
|
normalOnes
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rebuild the array in the right order based on element types
|
||||||
|
const boxes = this.reconstructBoxesInOrder(
|
||||||
|
elements,
|
||||||
|
categorized,
|
||||||
|
bigBoxes,
|
||||||
|
normalBoxes,
|
||||||
|
smallBoxes,
|
||||||
|
topBarBoxes
|
||||||
|
);
|
||||||
|
|
||||||
|
return { boxes, areas };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate best dimensions for a set of elements
|
||||||
|
* @param minRatio Minimum aspect ratio
|
||||||
|
* @param maxRatio Maximum aspect ratio
|
||||||
|
* @param width Available width
|
||||||
|
* @param height Available height
|
||||||
|
* @param count Number of elements
|
||||||
|
* @param maxWidth Maximum element width
|
||||||
|
* @param maxHeight Maximum element height
|
||||||
|
* @returns Best dimensions calculation result
|
||||||
|
*/
|
||||||
|
getBestDimensions(
|
||||||
|
minRatio: number,
|
||||||
|
maxRatio: number,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
count: number,
|
||||||
|
maxWidth: number,
|
||||||
|
maxHeight: number
|
||||||
|
): BestDimensions {
|
||||||
|
// Cache key for memoization
|
||||||
|
const cacheKey = LayoutDimensionsCache.generateKey(minRatio, maxRatio, width, height, count, maxWidth, maxHeight);
|
||||||
|
const cached = this.dimensionsCache.get(cacheKey);
|
||||||
|
if (cached) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
let maxArea: number | undefined;
|
||||||
|
let targetCols = 1;
|
||||||
|
let targetRows = 1;
|
||||||
|
let targetHeight = 0;
|
||||||
|
let targetWidth = 0;
|
||||||
|
|
||||||
|
// Iterate through every possible combination of rows and columns
|
||||||
|
// and see which one has the least amount of whitespace
|
||||||
|
for (let i = 1; i <= count; i++) {
|
||||||
|
const cols = i;
|
||||||
|
const rows = Math.ceil(count / cols);
|
||||||
|
|
||||||
|
// Try taking up the whole height and width
|
||||||
|
let tHeight = Math.floor(height / rows);
|
||||||
|
let tWidth = Math.floor(width / cols);
|
||||||
|
|
||||||
|
let tRatio = tHeight / tWidth;
|
||||||
|
if (tRatio > maxRatio) {
|
||||||
|
// We went over decrease the height
|
||||||
|
tRatio = maxRatio;
|
||||||
|
tHeight = tWidth * tRatio;
|
||||||
|
} else if (tRatio < minRatio) {
|
||||||
|
// We went under decrease the width
|
||||||
|
tRatio = minRatio;
|
||||||
|
tWidth = tHeight / tRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
tWidth = Math.min(maxWidth, tWidth);
|
||||||
|
tHeight = Math.min(maxHeight, tHeight);
|
||||||
|
|
||||||
|
const area = tWidth * tHeight * count;
|
||||||
|
|
||||||
|
// If this width and height takes up the most space then we're going with that
|
||||||
|
if (maxArea === undefined || area >= maxArea) {
|
||||||
|
if (!(area === maxArea && count % (cols * rows) > count % (targetRows * targetCols))) {
|
||||||
|
// Favour even numbers of participants in each row, eg. 2 on each row
|
||||||
|
// instead of 3 in one row and then 1 on the next
|
||||||
|
maxArea = area;
|
||||||
|
targetHeight = tHeight;
|
||||||
|
targetWidth = tWidth;
|
||||||
|
targetCols = cols;
|
||||||
|
targetRows = rows;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: BestDimensions = {
|
||||||
|
maxArea: maxArea || 0,
|
||||||
|
targetCols: targetCols,
|
||||||
|
targetRows: targetRows,
|
||||||
|
targetHeight: targetHeight,
|
||||||
|
targetWidth: targetWidth,
|
||||||
|
ratio: targetHeight / targetWidth || 0
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cache the result for future use
|
||||||
|
this.dimensionsCache.set(cacheKey, result);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate boxes for a specific area
|
||||||
|
* @param opts Area-specific layout options
|
||||||
|
* @param elements Elements to position in this area
|
||||||
|
* @returns Array of layout boxes
|
||||||
|
*/
|
||||||
|
private calculateBoxesForArea(
|
||||||
|
opts: Partial<ExtendedLayoutOptions & { offsetLeft: number; offsetTop: number }>,
|
||||||
|
elements: ElementDimensions[]
|
||||||
|
): LayoutBox[] {
|
||||||
|
const {
|
||||||
|
maxRatio = LAYOUT_CONSTANTS.DEFAULT_MAX_RATIO,
|
||||||
|
minRatio = LAYOUT_CONSTANTS.DEFAULT_MIN_RATIO,
|
||||||
|
fixedRatio = false,
|
||||||
|
containerWidth = LAYOUT_CONSTANTS.DEFAULT_VIDEO_WIDTH,
|
||||||
|
containerHeight = LAYOUT_CONSTANTS.DEFAULT_VIDEO_HEIGHT,
|
||||||
|
offsetLeft = 0,
|
||||||
|
offsetTop = 0,
|
||||||
|
alignItems = LayoutAlignment.CENTER,
|
||||||
|
maxWidth = Infinity,
|
||||||
|
maxHeight = Infinity,
|
||||||
|
scaleLastRow = true
|
||||||
|
} = opts;
|
||||||
|
|
||||||
|
const ratios = elements.map((element) => element.height / element.width);
|
||||||
|
const count = ratios.length;
|
||||||
|
|
||||||
|
let dimensions;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop through each stream in the container and place it inside
|
||||||
|
let x = 0;
|
||||||
|
let y = 0;
|
||||||
|
const rows: LayoutRow[] = [];
|
||||||
|
let row: LayoutRow | undefined;
|
||||||
|
const boxes: LayoutBox[] = [];
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
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 LayoutAlignment.START:
|
||||||
|
y = 0;
|
||||||
|
break;
|
||||||
|
case LayoutAlignment.END:
|
||||||
|
y = containerHeight - totalRowHeight;
|
||||||
|
break;
|
||||||
|
case LayoutAlignment.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 LayoutAlignment.START:
|
||||||
|
rowMarginLeft = 0;
|
||||||
|
break;
|
||||||
|
case LayoutAlignment.END:
|
||||||
|
rowMarginLeft = containerWidth - row.width;
|
||||||
|
break;
|
||||||
|
case LayoutAlignment.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];
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
boxes.push({
|
||||||
|
left: x + offsetLeft,
|
||||||
|
top: y + offsetTop,
|
||||||
|
width: targetWidth,
|
||||||
|
height: targetHeight
|
||||||
|
});
|
||||||
|
x += targetWidth;
|
||||||
|
}
|
||||||
|
y += targetHeight;
|
||||||
|
}
|
||||||
|
return boxes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Categorize elements into big, normal, small, and topBar
|
||||||
|
* @param elements Elements to categorize
|
||||||
|
* @returns Categorized elements with their indices
|
||||||
|
*/
|
||||||
|
private categorizeElements(elements: ElementDimensions[]): CategorizedElements & {
|
||||||
|
bigOnes: ElementDimensions[];
|
||||||
|
normalOnes: ElementDimensions[];
|
||||||
|
smallOnes: ElementDimensions[];
|
||||||
|
topBarOnes: ElementDimensions[];
|
||||||
|
} {
|
||||||
|
const bigIndices: number[] = [];
|
||||||
|
const smallIndices: number[] = [];
|
||||||
|
const topBarIndices: number[] = [];
|
||||||
|
const normalIndices: number[] = [];
|
||||||
|
|
||||||
|
const bigOnes = elements.filter((element, idx) => {
|
||||||
|
if (element.big) {
|
||||||
|
bigIndices.push(idx);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const topBarOnes = elements.filter((element, idx) => {
|
||||||
|
if (!element.big && element.topBar) {
|
||||||
|
topBarIndices.push(idx);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const smallOnes = elements.filter((element, idx) => {
|
||||||
|
if (!element.big && !element.topBar && element.small) {
|
||||||
|
smallIndices.push(idx);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const normalOnes = elements.filter((element, idx) => {
|
||||||
|
if (!element.big && !element.topBar && !element.small) {
|
||||||
|
normalIndices.push(idx);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
big: bigOnes,
|
||||||
|
normal: normalOnes,
|
||||||
|
small: smallOnes,
|
||||||
|
topBar: topBarOnes,
|
||||||
|
bigOnes,
|
||||||
|
normalOnes,
|
||||||
|
smallOnes,
|
||||||
|
topBarOnes,
|
||||||
|
bigIndices,
|
||||||
|
normalIndices,
|
||||||
|
smallIndices,
|
||||||
|
topBarIndices
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reconstruct boxes in original element order
|
||||||
|
* @param elements Original elements
|
||||||
|
* @param categorized Categorized elements
|
||||||
|
* @param bigBoxes Boxes for big elements
|
||||||
|
* @param normalBoxes Boxes for normal elements
|
||||||
|
* @param smallBoxes Boxes for small elements
|
||||||
|
* @param topBarBoxes Boxes for topBar elements
|
||||||
|
* @returns Boxes in original order
|
||||||
|
*/
|
||||||
|
private reconstructBoxesInOrder(
|
||||||
|
elements: ElementDimensions[],
|
||||||
|
categorized: CategorizedElements,
|
||||||
|
bigBoxes: LayoutBox[],
|
||||||
|
normalBoxes: LayoutBox[],
|
||||||
|
smallBoxes: LayoutBox[],
|
||||||
|
topBarBoxes: LayoutBox[]
|
||||||
|
): LayoutBox[] {
|
||||||
|
const boxes: LayoutBox[] = [];
|
||||||
|
let bigBoxesIdx = 0;
|
||||||
|
let normalBoxesIdx = 0;
|
||||||
|
let smallBoxesIdx = 0;
|
||||||
|
let topBarBoxesIdx = 0;
|
||||||
|
|
||||||
|
elements.forEach((element, idx) => {
|
||||||
|
if (categorized.bigIndices.indexOf(idx) > -1) {
|
||||||
|
boxes[idx] = bigBoxes[bigBoxesIdx];
|
||||||
|
bigBoxesIdx += 1;
|
||||||
|
} else if (categorized.topBarIndices.indexOf(idx) > -1) {
|
||||||
|
boxes[idx] = topBarBoxes[topBarBoxesIdx];
|
||||||
|
topBarBoxesIdx += 1;
|
||||||
|
} else if (categorized.smallIndices.indexOf(idx) > -1) {
|
||||||
|
boxes[idx] = smallBoxes[smallBoxesIdx];
|
||||||
|
smallBoxesIdx += 1;
|
||||||
|
} else {
|
||||||
|
boxes[idx] = normalBoxes[normalBoxesIdx];
|
||||||
|
normalBoxesIdx += 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return boxes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get video aspect ratio
|
||||||
|
* @param element Element dimensions
|
||||||
|
* @returns Aspect ratio (height/width)
|
||||||
|
*/
|
||||||
|
private getVideoRatio(element: ElementDimensions): number {
|
||||||
|
return element.height / element.width;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
import { BestDimensions } from './layout-types.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages caching of dimension calculations for layout optimization.
|
||||||
|
* Uses memoization to avoid recalculating the same layout dimensions.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export class LayoutDimensionsCache {
|
||||||
|
private cache = new Map<string, BestDimensions>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves cached dimension calculation if available
|
||||||
|
* @param key Cache key generated from calculation parameters
|
||||||
|
* @returns Cached dimensions or undefined if not found
|
||||||
|
*/
|
||||||
|
get(key: string): BestDimensions | undefined {
|
||||||
|
return this.cache.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores dimension calculation result in cache
|
||||||
|
* @param key Cache key generated from calculation parameters
|
||||||
|
* @param value Calculated best dimensions
|
||||||
|
*/
|
||||||
|
set(key: string, value: BestDimensions): void {
|
||||||
|
this.cache.set(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears all cached dimensions to free memory
|
||||||
|
*/
|
||||||
|
clear(): void {
|
||||||
|
this.cache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current cache size
|
||||||
|
* @returns Number of cached entries
|
||||||
|
*/
|
||||||
|
size(): number {
|
||||||
|
return this.cache.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a key exists in the cache
|
||||||
|
* @param key Cache key to check
|
||||||
|
* @returns True if key exists
|
||||||
|
*/
|
||||||
|
has(key: string): boolean {
|
||||||
|
return this.cache.has(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a cache key from calculation parameters
|
||||||
|
* @param minRatio Minimum aspect ratio
|
||||||
|
* @param maxRatio Maximum aspect ratio
|
||||||
|
* @param width Container width
|
||||||
|
* @param height Container height
|
||||||
|
* @param count Number of elements
|
||||||
|
* @param maxWidth Maximum element width
|
||||||
|
* @param maxHeight Maximum element height
|
||||||
|
* @returns Cache key string
|
||||||
|
*/
|
||||||
|
static generateKey(
|
||||||
|
minRatio: number,
|
||||||
|
maxRatio: number,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
count: number,
|
||||||
|
maxWidth: number,
|
||||||
|
maxHeight: number
|
||||||
|
): string {
|
||||||
|
return `${minRatio}_${maxRatio}_${width}_${height}_${count}_${maxWidth}_${maxHeight}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,196 @@
|
||||||
|
import { LAYOUT_CONSTANTS, LayoutBox } from './layout-types.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Position information for DOM element
|
||||||
|
*/
|
||||||
|
interface ElementPosition {
|
||||||
|
left: string;
|
||||||
|
top: string;
|
||||||
|
width: string;
|
||||||
|
height: string;
|
||||||
|
[key: string]: string; // Allow index signature for dynamic access
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles DOM manipulation and rendering for layout elements.
|
||||||
|
* Manages positioning, animations, and visual updates without calculation logic.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export class LayoutRenderer {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render layout boxes to DOM elements
|
||||||
|
* @param container Parent container element
|
||||||
|
* @param boxes Calculated layout boxes
|
||||||
|
* @param elements DOM elements to position
|
||||||
|
* @param animate Whether to animate transitions
|
||||||
|
*/
|
||||||
|
renderLayout(container: HTMLElement, boxes: LayoutBox[], elements: HTMLElement[], animate: boolean): void {
|
||||||
|
boxes.forEach((box, idx) => {
|
||||||
|
const elem = elements[idx];
|
||||||
|
if (!elem) return;
|
||||||
|
|
||||||
|
// Set position:absolute for proper layout positioning
|
||||||
|
this.getCssProperty(elem, 'position', 'absolute');
|
||||||
|
|
||||||
|
const actualDimensions = this.calculateActualDimensions(elem, box);
|
||||||
|
this.positionElement(elem, box.left, box.top, actualDimensions.width, actualDimensions.height, animate);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate actual element dimensions accounting for margins, padding, and borders
|
||||||
|
* @param elem DOM element
|
||||||
|
* @param box Layout box dimensions
|
||||||
|
* @returns Actual width and height
|
||||||
|
*/
|
||||||
|
private calculateActualDimensions(elem: HTMLElement, box: LayoutBox): { width: number; height: number } {
|
||||||
|
const actualWidth =
|
||||||
|
box.width -
|
||||||
|
this.getCSSNumber(elem, 'margin-left') -
|
||||||
|
this.getCSSNumber(elem, 'margin-right') -
|
||||||
|
(this.getCssProperty(elem, 'box-sizing') !== 'border-box'
|
||||||
|
? this.getCSSNumber(elem, 'padding-left') +
|
||||||
|
this.getCSSNumber(elem, 'padding-right') +
|
||||||
|
this.getCSSNumber(elem, 'border-left') +
|
||||||
|
this.getCSSNumber(elem, 'border-right')
|
||||||
|
: 0);
|
||||||
|
|
||||||
|
const actualHeight =
|
||||||
|
box.height -
|
||||||
|
this.getCSSNumber(elem, 'margin-top') -
|
||||||
|
this.getCSSNumber(elem, 'margin-bottom') -
|
||||||
|
(this.getCssProperty(elem, 'box-sizing') !== 'border-box'
|
||||||
|
? this.getCSSNumber(elem, 'padding-top') +
|
||||||
|
this.getCSSNumber(elem, 'padding-bottom') +
|
||||||
|
this.getCSSNumber(elem, 'border-top') +
|
||||||
|
this.getCSSNumber(elem, 'border-bottom')
|
||||||
|
: 0);
|
||||||
|
|
||||||
|
return { width: actualWidth, height: actualHeight };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Position element at specified coordinates with optional animation
|
||||||
|
* @param elem Video or HTML element to position
|
||||||
|
* @param x Left position
|
||||||
|
* @param y Top position
|
||||||
|
* @param width Element width
|
||||||
|
* @param height Element height
|
||||||
|
* @param animate Whether to animate the transition
|
||||||
|
*/
|
||||||
|
private positionElement(
|
||||||
|
elem: HTMLVideoElement | HTMLElement,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
animate: boolean
|
||||||
|
): void {
|
||||||
|
const targetPosition: ElementPosition = {
|
||||||
|
left: `${x}px`,
|
||||||
|
top: `${y}px`,
|
||||||
|
width: `${width}px`,
|
||||||
|
height: `${height}px`
|
||||||
|
};
|
||||||
|
|
||||||
|
this.fixAspectRatio(elem, width);
|
||||||
|
|
||||||
|
if (animate) {
|
||||||
|
setTimeout(() => {
|
||||||
|
// Animation added in CSS transition: all .1s linear;
|
||||||
|
this.animateElement(elem, targetPosition);
|
||||||
|
this.fixAspectRatio(elem, width);
|
||||||
|
}, 10);
|
||||||
|
} else {
|
||||||
|
this.setElementPosition(elem, targetPosition);
|
||||||
|
if (!elem.classList.contains('layout')) {
|
||||||
|
elem.classList.add('layout');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.fixAspectRatio(elem, width);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set element position without animation
|
||||||
|
* @param elem Element to position
|
||||||
|
* @param targetPosition Target position object
|
||||||
|
*/
|
||||||
|
private setElementPosition(elem: HTMLVideoElement | HTMLElement, targetPosition: ElementPosition): void {
|
||||||
|
Object.keys(targetPosition).forEach((key) => {
|
||||||
|
(elem.style as any)[key] = targetPosition[key];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Animate element to target position
|
||||||
|
* @param elem Element to animate
|
||||||
|
* @param targetPosition Target position object
|
||||||
|
*/
|
||||||
|
private animateElement(elem: HTMLVideoElement | HTMLElement, targetPosition: ElementPosition): void {
|
||||||
|
elem.style.transition = `all ${LAYOUT_CONSTANTS.ANIMATION_DURATION} ${LAYOUT_CONSTANTS.ANIMATION_EASING}`;
|
||||||
|
this.setElementPosition(elem, targetPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fix aspect ratio for video elements
|
||||||
|
* @param elem Element to fix
|
||||||
|
* @param width Target width
|
||||||
|
*/
|
||||||
|
private fixAspectRatio(elem: HTMLVideoElement | HTMLElement, width: number): void {
|
||||||
|
const sub = elem.querySelector('.OV_root') as HTMLVideoElement;
|
||||||
|
if (sub) {
|
||||||
|
// If this is the parent of a subscriber or publisher, then we need
|
||||||
|
// to force the mutation observer on the publisher or subscriber to
|
||||||
|
// trigger to get it to fix its layout
|
||||||
|
const oldWidth = sub.style.width;
|
||||||
|
sub.style.width = `${width}px`;
|
||||||
|
sub.style.width = oldWidth || '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get CSS property value or set it
|
||||||
|
* @param el Element to query/modify
|
||||||
|
* @param propertyName Property name or object of properties
|
||||||
|
* @param value Optional value to set
|
||||||
|
* @returns Property value if getting, void if setting
|
||||||
|
*/
|
||||||
|
private getCssProperty(
|
||||||
|
el: HTMLVideoElement | HTMLElement,
|
||||||
|
propertyName: any,
|
||||||
|
value?: string
|
||||||
|
): void | string {
|
||||||
|
if (value !== undefined) {
|
||||||
|
// Set one CSS property
|
||||||
|
el.style[propertyName] = value;
|
||||||
|
} else if (typeof propertyName === 'object') {
|
||||||
|
// Set several CSS properties at once
|
||||||
|
Object.keys(propertyName).forEach((key) => {
|
||||||
|
this.getCssProperty(el, key, propertyName[key]);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Get the CSS property
|
||||||
|
const computedStyle = window.getComputedStyle(el);
|
||||||
|
let currentValue = computedStyle.getPropertyValue(propertyName);
|
||||||
|
|
||||||
|
if (currentValue === '') {
|
||||||
|
currentValue = el.style[propertyName];
|
||||||
|
}
|
||||||
|
return currentValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get CSS property as number
|
||||||
|
* @param elem Element to query
|
||||||
|
* @param prop Property name
|
||||||
|
* @returns Numeric value or 0 if not found
|
||||||
|
*/
|
||||||
|
private getCSSNumber(elem: HTMLElement, prop: string): number {
|
||||||
|
const cssStr = this.getCssProperty(elem, prop);
|
||||||
|
return cssStr ? parseInt(cssStr, 10) : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,192 @@
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export enum LayoutClass {
|
||||||
|
ROOT_ELEMENT = 'OV_root',
|
||||||
|
BIG_ELEMENT = 'OV_big',
|
||||||
|
SMALL_ELEMENT = 'OV_small',
|
||||||
|
TOP_BAR_ELEMENT = 'OV_top-bar',
|
||||||
|
IGNORED_ELEMENT = 'OV_ignored',
|
||||||
|
MINIMIZED_ELEMENT = 'OV_minimized',
|
||||||
|
SIDENAV_CONTAINER = 'sidenav-container',
|
||||||
|
NO_SIZE_ELEMENT = 'no-size',
|
||||||
|
CLASS_NAME = 'layout'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export enum SidenavMode {
|
||||||
|
OVER = 'over',
|
||||||
|
SIDE = 'side'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export enum LayoutAlignment {
|
||||||
|
START = 'start',
|
||||||
|
CENTER = 'center',
|
||||||
|
END = 'end'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Layout position options for big elements
|
||||||
|
*/
|
||||||
|
export type BigFirstOption = boolean | 'column' | 'row';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Element dimensions interface
|
||||||
|
*/
|
||||||
|
export interface ElementDimensions {
|
||||||
|
height: number;
|
||||||
|
width: number;
|
||||||
|
big?: boolean;
|
||||||
|
small?: boolean;
|
||||||
|
topBar?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Layout area definition
|
||||||
|
*/
|
||||||
|
export interface LayoutArea {
|
||||||
|
top: number;
|
||||||
|
left: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Layout box positioning
|
||||||
|
*/
|
||||||
|
export interface LayoutBox extends LayoutArea {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Row structure for layout calculations
|
||||||
|
*/
|
||||||
|
export interface LayoutRow {
|
||||||
|
ratios: number[];
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Best dimensions calculation result
|
||||||
|
*/
|
||||||
|
export interface BestDimensions {
|
||||||
|
maxArea: number;
|
||||||
|
targetCols: number;
|
||||||
|
targetRows: number;
|
||||||
|
targetHeight: number;
|
||||||
|
targetWidth: number;
|
||||||
|
ratio: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extended layout options with container dimensions
|
||||||
|
*/
|
||||||
|
export interface ExtendedLayoutOptions extends OpenViduLayoutOptions {
|
||||||
|
containerWidth: number;
|
||||||
|
containerHeight: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Layout calculation result containing positioned boxes and allocated areas
|
||||||
|
*/
|
||||||
|
export interface LayoutCalculationResult {
|
||||||
|
boxes: LayoutBox[];
|
||||||
|
areas: LayoutAreas;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Layout areas for different element categories
|
||||||
|
*/
|
||||||
|
export interface LayoutAreas {
|
||||||
|
big: LayoutArea | null;
|
||||||
|
normal: LayoutArea | null;
|
||||||
|
small: LayoutArea | null;
|
||||||
|
topBar: LayoutArea | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Categorized elements by type
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export interface CategorizedElements {
|
||||||
|
big: ElementDimensions[];
|
||||||
|
normal: ElementDimensions[];
|
||||||
|
small: ElementDimensions[];
|
||||||
|
topBar: ElementDimensions[];
|
||||||
|
bigIndices: number[];
|
||||||
|
normalIndices: number[];
|
||||||
|
smallIndices: number[];
|
||||||
|
topBarIndices: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Layout configuration constants
|
||||||
|
*/
|
||||||
|
export const LAYOUT_CONSTANTS = {
|
||||||
|
DEFAULT_VIDEO_WIDTH: 640,
|
||||||
|
DEFAULT_VIDEO_HEIGHT: 480,
|
||||||
|
DEFAULT_MAX_RATIO: 3 / 2,
|
||||||
|
DEFAULT_MIN_RATIO: 9 / 16,
|
||||||
|
DEFAULT_BIG_PERCENTAGE: 0.8,
|
||||||
|
UPDATE_TIMEOUT: 50,
|
||||||
|
ANIMATION_DURATION: '0.1s',
|
||||||
|
ANIMATION_EASING: 'linear'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export interface OpenViduLayoutOptions {
|
||||||
|
/** The narrowest ratio that will be used (2x3 by default) */
|
||||||
|
maxRatio: number;
|
||||||
|
/** The widest ratio that will be used (16x9 by default) */
|
||||||
|
minRatio: number;
|
||||||
|
/** If true, aspect ratio is maintained and minRatio/maxRatio are ignored */
|
||||||
|
fixedRatio: boolean;
|
||||||
|
/** Whether to animate transitions */
|
||||||
|
animate: boolean;
|
||||||
|
/** Class for elements that should be sized bigger */
|
||||||
|
bigClass: string;
|
||||||
|
/** Class for elements that should be sized smaller */
|
||||||
|
smallClass: string;
|
||||||
|
/** Class for elements that should be ignored */
|
||||||
|
ignoredClass: string;
|
||||||
|
/** Maximum percentage of space big elements should take up */
|
||||||
|
bigPercentage: number;
|
||||||
|
/** Minimum percentage for big space to scale down whitespace */
|
||||||
|
minBigPercentage: number;
|
||||||
|
/** Fixed ratio for big elements */
|
||||||
|
bigFixedRatio: boolean;
|
||||||
|
/** Narrowest ratio for big elements */
|
||||||
|
bigMaxRatio: number;
|
||||||
|
/** Widest ratio for big elements */
|
||||||
|
bigMinRatio: number;
|
||||||
|
/** Position preference for big elements */
|
||||||
|
bigFirst: BigFirstOption;
|
||||||
|
/** Alignment for all elements */
|
||||||
|
alignItems: LayoutAlignment;
|
||||||
|
/** Alignment for big elements */
|
||||||
|
bigAlignItems: LayoutAlignment;
|
||||||
|
/** Alignment for small elements */
|
||||||
|
smallAlignItems: LayoutAlignment;
|
||||||
|
/** Maximum width of elements */
|
||||||
|
maxWidth: number;
|
||||||
|
/** Maximum height of elements */
|
||||||
|
maxHeight: number;
|
||||||
|
/** Maximum width for small elements */
|
||||||
|
smallMaxWidth: number;
|
||||||
|
/** Maximum height for small elements */
|
||||||
|
smallMaxHeight: number;
|
||||||
|
/** Maximum width for big elements */
|
||||||
|
bigMaxWidth: number;
|
||||||
|
/** Maximum height for big elements */
|
||||||
|
bigMaxHeight: number;
|
||||||
|
/** Scale up elements in last row if fewer elements */
|
||||||
|
scaleLastRow: boolean;
|
||||||
|
/** Scale up big elements in last row */
|
||||||
|
bigScaleLastRow: boolean;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,222 @@
|
||||||
|
// Re-export all public types and constants for backward compatibility
|
||||||
|
export {
|
||||||
|
BestDimensions,
|
||||||
|
BigFirstOption,
|
||||||
|
ElementDimensions,
|
||||||
|
ExtendedLayoutOptions,
|
||||||
|
LAYOUT_CONSTANTS,
|
||||||
|
LayoutAlignment,
|
||||||
|
LayoutArea,
|
||||||
|
LayoutBox,
|
||||||
|
LayoutClass,
|
||||||
|
LayoutRow,
|
||||||
|
OpenViduLayoutOptions,
|
||||||
|
SidenavMode
|
||||||
|
} from './layout-types.model';
|
||||||
|
|
||||||
|
import { LayoutCalculator } from './layout-calculator.model';
|
||||||
|
import { LayoutDimensionsCache } from './layout-dimensions-cache.model';
|
||||||
|
import { LayoutRenderer } from './layout-renderer.model';
|
||||||
|
import { ElementDimensions, ExtendedLayoutOptions, LAYOUT_CONSTANTS, LayoutClass, OpenViduLayoutOptions } from './layout-types.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OpenViduLayout orchestrates layout calculation and rendering.
|
||||||
|
* Maintains backward compatibility with existing API while delegating to specialized classes.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export class OpenViduLayout {
|
||||||
|
private layoutContainer!: HTMLElement;
|
||||||
|
private opts!: OpenViduLayoutOptions;
|
||||||
|
|
||||||
|
// Specialized components
|
||||||
|
private dimensionsCache: LayoutDimensionsCache;
|
||||||
|
private calculator: LayoutCalculator;
|
||||||
|
private renderer: LayoutRenderer;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.dimensionsCache = new LayoutDimensionsCache();
|
||||||
|
this.calculator = new LayoutCalculator(this.dimensionsCache);
|
||||||
|
this.renderer = new LayoutRenderer();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the layout container
|
||||||
|
* module export layout
|
||||||
|
*/
|
||||||
|
updateLayout(container: HTMLElement, opts: OpenViduLayoutOptions) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.layoutContainer = container;
|
||||||
|
this.opts = opts;
|
||||||
|
|
||||||
|
if (this.getCssProperty(this.layoutContainer, 'display') === 'none') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let id = this.layoutContainer.id;
|
||||||
|
if (!id) {
|
||||||
|
id = 'OV_' + this.cheapUUID();
|
||||||
|
this.layoutContainer.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const extendedOpts: ExtendedLayoutOptions = {
|
||||||
|
...opts,
|
||||||
|
containerHeight:
|
||||||
|
this.getHeight(this.layoutContainer) -
|
||||||
|
this.getCSSNumber(this.layoutContainer, 'border-top') -
|
||||||
|
this.getCSSNumber(this.layoutContainer, 'border-bottom'),
|
||||||
|
containerWidth:
|
||||||
|
this.getWidth(this.layoutContainer) -
|
||||||
|
this.getCSSNumber(this.layoutContainer, 'border-left') -
|
||||||
|
this.getCSSNumber(this.layoutContainer, 'border-right')
|
||||||
|
};
|
||||||
|
|
||||||
|
const selector = `#${id}>*:not(.${LayoutClass.IGNORED_ELEMENT}):not(.${LayoutClass.MINIMIZED_ELEMENT})`;
|
||||||
|
const children = Array.prototype.filter.call(this.layoutContainer.querySelectorAll(selector), () => this.filterDisplayNone);
|
||||||
|
|
||||||
|
const elements = children.map((element: HTMLElement) => {
|
||||||
|
const res = this.getChildDims(element);
|
||||||
|
res.big = element.classList.contains(this.opts.bigClass);
|
||||||
|
res.small = element.classList.contains(LayoutClass.SMALL_ELEMENT);
|
||||||
|
res.topBar = element.classList.contains(LayoutClass.TOP_BAR_ELEMENT);
|
||||||
|
return res;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delegate calculation to LayoutCalculator
|
||||||
|
const layout = this.calculator.calculateLayout(extendedOpts, elements);
|
||||||
|
|
||||||
|
// Delegate rendering to LayoutRenderer
|
||||||
|
this.renderer.renderLayout(this.layoutContainer, layout.boxes, children, this.opts.animate);
|
||||||
|
}, LAYOUT_CONSTANTS.UPDATE_TIMEOUT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the layout inside of the container with the options required
|
||||||
|
* @param container
|
||||||
|
* @param opts
|
||||||
|
*/
|
||||||
|
initLayoutContainer(container: HTMLElement, opts: OpenViduLayoutOptions) {
|
||||||
|
this.opts = opts;
|
||||||
|
this.layoutContainer = container;
|
||||||
|
this.updateLayout(container, opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
getLayoutContainer(): HTMLElement {
|
||||||
|
return this.layoutContainer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear dimensions cache to free memory
|
||||||
|
*/
|
||||||
|
clearCache(): void {
|
||||||
|
this.dimensionsCache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// PRIVATE UTILITY METHODS (DOM Helpers)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
private getCssProperty(el: HTMLVideoElement | HTMLElement, propertyName: any, value?: string): void | string {
|
||||||
|
if (value !== undefined) {
|
||||||
|
// Set one CSS property
|
||||||
|
el.style[propertyName] = value;
|
||||||
|
} else if (typeof propertyName === 'object') {
|
||||||
|
// Set several CSS properties at once
|
||||||
|
Object.keys(propertyName).forEach((key) => {
|
||||||
|
this.getCssProperty(el, key, propertyName[key]);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Get the CSS property
|
||||||
|
const computedStyle = window.getComputedStyle(el);
|
||||||
|
let currentValue = computedStyle.getPropertyValue(propertyName);
|
||||||
|
|
||||||
|
if (currentValue === '') {
|
||||||
|
currentValue = el.style[propertyName];
|
||||||
|
}
|
||||||
|
return currentValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private height(el: HTMLElement) {
|
||||||
|
const { offsetHeight } = el;
|
||||||
|
|
||||||
|
if (offsetHeight > 0) {
|
||||||
|
return `${offsetHeight}px`;
|
||||||
|
}
|
||||||
|
return this.getCssProperty(el, 'height');
|
||||||
|
}
|
||||||
|
|
||||||
|
private width(el: HTMLElement) {
|
||||||
|
const { offsetWidth } = el;
|
||||||
|
|
||||||
|
if (offsetWidth > 0) {
|
||||||
|
return `${offsetWidth}px`;
|
||||||
|
}
|
||||||
|
return this.getCssProperty(el, 'width');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @hidden
|
||||||
|
*/
|
||||||
|
private getChildDims(child: HTMLVideoElement | HTMLElement): ElementDimensions {
|
||||||
|
if (child instanceof HTMLVideoElement) {
|
||||||
|
if (child.videoHeight && child.videoWidth) {
|
||||||
|
return {
|
||||||
|
height: child.videoHeight,
|
||||||
|
width: child.videoWidth
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else if (child instanceof HTMLElement) {
|
||||||
|
const video = child.querySelector('video');
|
||||||
|
if (video instanceof HTMLVideoElement && video.videoHeight && video.videoWidth) {
|
||||||
|
return {
|
||||||
|
height: video.videoHeight,
|
||||||
|
width: video.videoWidth
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
height: LAYOUT_CONSTANTS.DEFAULT_VIDEO_HEIGHT,
|
||||||
|
width: LAYOUT_CONSTANTS.DEFAULT_VIDEO_WIDTH
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @hidden
|
||||||
|
*/
|
||||||
|
private getCSSNumber(elem: HTMLElement, prop: string): number {
|
||||||
|
const cssStr = this.getCssProperty(elem, prop);
|
||||||
|
return cssStr ? parseInt(cssStr, 10) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @hidden
|
||||||
|
*/
|
||||||
|
// Really cheap UUID function
|
||||||
|
private cheapUUID(): string {
|
||||||
|
return Math.floor(Math.random() * 100000000).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @hidden
|
||||||
|
*/
|
||||||
|
private getHeight(elem: HTMLElement): number {
|
||||||
|
const heightStr = this.height(elem);
|
||||||
|
return heightStr ? parseInt(heightStr, 10) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @hidden
|
||||||
|
*/
|
||||||
|
private getWidth(elem: HTMLElement): number {
|
||||||
|
const widthStr = this.width(elem);
|
||||||
|
return widthStr ? parseInt(widthStr, 10) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @hidden
|
||||||
|
*/
|
||||||
|
private filterDisplayNone(element: HTMLElement) {
|
||||||
|
return this.getCssProperty(element, 'display') !== 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { LayoutClass } from '../../models/layout.model';
|
import { LayoutClass } from '../../models/layout/layout.model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Injectable, effect } from '@angular/core';
|
import { Injectable, effect } from '@angular/core';
|
||||||
import { BehaviorSubject, Observable } from 'rxjs';
|
import { BehaviorSubject, Observable } from 'rxjs';
|
||||||
import { LayoutAlignment, LayoutClass, OpenViduLayout, OpenViduLayoutOptions } from '../../models/layout.model';
|
import { LayoutAlignment, LayoutClass, OpenViduLayout, OpenViduLayoutOptions } from '../../models/layout/layout.model';
|
||||||
import { ILogger } from '../../models/logger.model';
|
import { ILogger } from '../../models/logger.model';
|
||||||
import { LoggerService } from '../logger/logger.service';
|
import { LoggerService } from '../logger/logger.service';
|
||||||
import { ViewportService } from '../viewport/viewport.service';
|
import { ViewportService } from '../viewport/viewport.service';
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ export * from './lib/models/broadcasting.model';
|
||||||
export * from './lib/models/data-topic.model';
|
export * from './lib/models/data-topic.model';
|
||||||
export * from './lib/models/device.model';
|
export * from './lib/models/device.model';
|
||||||
export * from './lib/models/lang.model';
|
export * from './lib/models/lang.model';
|
||||||
export * from './lib/models/layout.model';
|
export * from './lib/models/layout/layout.model';
|
||||||
export * from './lib/models/logger.model';
|
export * from './lib/models/logger.model';
|
||||||
export * from './lib/models/panel.model';
|
export * from './lib/models/panel.model';
|
||||||
export * from './lib/models/participant.model';
|
export * from './lib/models/participant.model';
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue