588 lines
16 KiB
TypeScript
588 lines
16 KiB
TypeScript
export enum LayoutClass {
|
|
ROOT_ELEMENT = 'OT_root',
|
|
BIG_ELEMENT = 'OV_big',
|
|
SMALL_ELEMENT = 'OV_small',
|
|
SIDENAV_CONTAINER = 'sidenav-container',
|
|
NO_SIZE_ELEMENT = 'no-size'
|
|
}
|
|
|
|
export enum SidenavMode {
|
|
OVER = 'over',
|
|
SIDE = 'side'
|
|
}
|
|
|
|
/**
|
|
* @hidden
|
|
*/
|
|
|
|
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 this is true then the aspect ratio of the video is maintained and minRatio and maxRatio are ignored (*false* by default)
|
|
*/
|
|
fixedRatio: boolean;
|
|
/**
|
|
* Whether you want to animate the transitions
|
|
*/
|
|
animate: any;
|
|
/**
|
|
* The class to add to elements that should be sized bigger
|
|
*/
|
|
bigClass: string;
|
|
|
|
/**
|
|
* The class to add to elements that should be sized smaller
|
|
*/
|
|
smallClass: string;
|
|
|
|
/**
|
|
* The maximum percentage of space the big ones should take up
|
|
*/
|
|
bigPercentage: any;
|
|
|
|
/**
|
|
* FixedRatio for the big ones
|
|
*/
|
|
bigFixedRatio: any;
|
|
|
|
/**
|
|
* The narrowest ratio to use for the big elements (*2x3* by default)
|
|
*/
|
|
bigMaxRatio: any;
|
|
|
|
/**
|
|
* The widest ratio to use for the big elements (*16x9* by default)
|
|
*/
|
|
bigMinRatio: any;
|
|
|
|
/**
|
|
* Whether to place the big one in the top left `true` or bottom right
|
|
*/
|
|
bigFirst: any;
|
|
}
|
|
|
|
export class OpenViduLayout {
|
|
/**
|
|
* @hidden
|
|
*/
|
|
private layoutContainer: HTMLElement;
|
|
|
|
/**
|
|
* @hidden
|
|
*/
|
|
private opts: OpenViduLayoutOptions;
|
|
|
|
/**
|
|
* Update the layout container
|
|
*/
|
|
updateLayout() {
|
|
setTimeout(() => {
|
|
if (this.layoutContainer?.style?.display === 'none') {
|
|
return;
|
|
}
|
|
let id = this.layoutContainer.id;
|
|
if (!id) {
|
|
id = 'OT_' + this.cheapUUID();
|
|
this.layoutContainer.id = id;
|
|
}
|
|
const smallOnes: HTMLVideoElement[] = Array.prototype.filter.call(
|
|
this.layoutContainer.querySelectorAll('#' + id + '>.' + this.opts.smallClass),
|
|
this.filterDisplayNone
|
|
);
|
|
const bigOnes: HTMLVideoElement[] = Array.prototype.filter
|
|
.call(this.layoutContainer.querySelectorAll('#' + id + '>.' + this.opts.bigClass), this.filterDisplayNone)
|
|
.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);
|
|
}, 50);
|
|
}
|
|
|
|
/**
|
|
* Initialize the layout inside of the container with the options required
|
|
* @param container
|
|
* @param opts
|
|
*/
|
|
initLayoutContainer(container: HTMLElement, opts: OpenViduLayoutOptions) {
|
|
this.opts = {
|
|
maxRatio: opts.maxRatio != null ? opts.maxRatio : 3 / 2,
|
|
minRatio: opts.minRatio != null ? opts.minRatio : 9 / 16,
|
|
fixedRatio: opts.fixedRatio != null ? opts.fixedRatio : false,
|
|
animate: opts.animate != null ? opts.animate : false,
|
|
bigClass: opts.bigClass != null ? opts.bigClass : 'OT_big',
|
|
smallClass: opts.smallClass != null ? opts.smallClass : 'OT_small',
|
|
bigPercentage: opts.bigPercentage != null ? opts.bigPercentage : 0.8,
|
|
bigFixedRatio: opts.bigFixedRatio != null ? opts.bigFixedRatio : false,
|
|
bigMaxRatio: opts.bigMaxRatio != null ? opts.bigMaxRatio : 3 / 2,
|
|
bigMinRatio: opts.bigMinRatio != null ? opts.bigMinRatio : 9 / 16,
|
|
bigFirst: opts.bigFirst != null ? opts.bigFirst : true
|
|
};
|
|
this.layoutContainer = container;
|
|
}
|
|
|
|
/**
|
|
* Set the layout configuration
|
|
* @param options
|
|
*/
|
|
setLayoutOptions(options: OpenViduLayoutOptions) {
|
|
this.opts = options;
|
|
}
|
|
|
|
getLayoutContainer(): HTMLElement {
|
|
return this.layoutContainer;
|
|
}
|
|
|
|
/**
|
|
* @hidden
|
|
*/
|
|
private fixAspectRatio(elem: HTMLVideoElement, width: number) {
|
|
const sub: HTMLVideoElement = <HTMLVideoElement>elem.querySelector('.OT_root');
|
|
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 it's layout
|
|
const oldWidth = sub.style.width;
|
|
sub.style.width = width + 'px';
|
|
// sub.style.height = height + 'px';
|
|
sub.style.width = oldWidth || '';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @hidden
|
|
*/
|
|
private positionElement(elem: HTMLVideoElement, x: number, y: number, width: number, height: number, animate: any) {
|
|
const targetPosition = {
|
|
left: x + 'px',
|
|
top: y + 'px',
|
|
width: width + 'px',
|
|
height: height + 'px'
|
|
};
|
|
|
|
this.fixAspectRatio(elem, width);
|
|
|
|
setTimeout(() => {
|
|
// animation added in css transition: all .1s linear;
|
|
elem.style.left = targetPosition.left;
|
|
elem.style.top = targetPosition.top;
|
|
elem.style.width = targetPosition.width;
|
|
elem.style.height = targetPosition.height;
|
|
this.fixAspectRatio(elem, width);
|
|
}, 10);
|
|
}
|
|
|
|
/**
|
|
* @hidden
|
|
*/
|
|
private getVideoRatio(elem: HTMLVideoElement) {
|
|
if (!elem) {
|
|
return 3 / 4;
|
|
}
|
|
const video: HTMLVideoElement = <HTMLVideoElement>elem.querySelector('video');
|
|
if (video && video.videoHeight && video.videoWidth) {
|
|
return video.videoHeight / video.videoWidth;
|
|
} else if (elem.videoHeight && elem.videoWidth) {
|
|
return elem.videoHeight / elem.videoWidth;
|
|
}
|
|
return 3 / 4;
|
|
}
|
|
|
|
/**
|
|
* @hidden
|
|
*/
|
|
private getCSSNumber(elem: HTMLElement, prop: string) {
|
|
const cssStr = window.getComputedStyle(elem)[prop];
|
|
|
|
return cssStr ? parseInt(cssStr, 10) : 0;
|
|
}
|
|
|
|
/**
|
|
* @hidden
|
|
*/
|
|
// Really cheap UUID function
|
|
private cheapUUID() {
|
|
return (Math.random() * 100000000).toFixed(0);
|
|
}
|
|
|
|
/**
|
|
* @hidden
|
|
*/
|
|
private getHeight(elem: HTMLElement) {
|
|
const heightStr = window.getComputedStyle(elem)['height'];
|
|
return heightStr ? parseInt(heightStr, 10) : 0;
|
|
}
|
|
|
|
/**
|
|
* @hidden
|
|
*/
|
|
private getWidth(elem: HTMLElement) {
|
|
const widthStr = window.getComputedStyle(elem)['width'];
|
|
return widthStr ? parseInt(widthStr, 10) : 0;
|
|
}
|
|
|
|
/**
|
|
* @hidden
|
|
*/
|
|
private getBestDimensions(minR: number, maxR: number, count: number, WIDTH: number, HEIGHT: number, targetHeight: number) {
|
|
let maxArea, targetCols, targetRows, targetWidth, tWidth, tHeight, tRatio;
|
|
|
|
// 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 colsAux = i;
|
|
const rowsAux = Math.ceil(count / colsAux);
|
|
|
|
// Try taking up the whole height and width
|
|
tHeight = Math.floor(HEIGHT / rowsAux);
|
|
tWidth = Math.floor(WIDTH / colsAux);
|
|
|
|
tRatio = tHeight / tWidth;
|
|
if (tRatio > maxR) {
|
|
// We went over decrease the height
|
|
tRatio = maxR;
|
|
tHeight = tWidth * tRatio;
|
|
} else if (tRatio < minR) {
|
|
// We went under decrease the width
|
|
tRatio = minR;
|
|
tWidth = tHeight / tRatio;
|
|
}
|
|
|
|
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) {
|
|
maxArea = area;
|
|
targetHeight = tHeight;
|
|
targetWidth = tWidth;
|
|
targetCols = colsAux;
|
|
targetRows = rowsAux;
|
|
}
|
|
}
|
|
return {
|
|
maxArea: maxArea,
|
|
targetCols: targetCols,
|
|
targetRows: targetRows,
|
|
targetHeight: targetHeight,
|
|
targetWidth: targetWidth,
|
|
ratio: targetHeight / targetWidth
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @hidden
|
|
*/
|
|
private arrange(
|
|
children: HTMLVideoElement[],
|
|
WIDTH: number,
|
|
HEIGHT: number,
|
|
offsetLeft: number,
|
|
offsetTop: number,
|
|
fixedRatio: boolean,
|
|
minRatio: number,
|
|
maxRatio: number,
|
|
animate: any
|
|
) {
|
|
let targetHeight;
|
|
|
|
const count = children.length;
|
|
let dimensions;
|
|
|
|
if (!fixedRatio) {
|
|
dimensions = this.getBestDimensions(minRatio, maxRatio, count, WIDTH, HEIGHT, targetHeight);
|
|
} else {
|
|
// Use the ratio of the first video element we find to approximate
|
|
const ratio = this.getVideoRatio(children.length > 0 ? children[0] : null);
|
|
dimensions = this.getBestDimensions(ratio, ratio, count, WIDTH, HEIGHT, targetHeight);
|
|
}
|
|
|
|
// Loop through each stream in the container and place it inside
|
|
let x = 0,
|
|
y = 0;
|
|
const rows = [];
|
|
let row;
|
|
// 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 < children.length; i++) {
|
|
if (i % dimensions.targetCols === 0) {
|
|
// This is a new row
|
|
row = {
|
|
children: [],
|
|
width: 0,
|
|
height: 0
|
|
};
|
|
rows.push(row);
|
|
}
|
|
const elem: HTMLVideoElement = children[i];
|
|
row.children.push(elem);
|
|
let targetWidth = dimensions.targetWidth;
|
|
targetHeight = dimensions.targetHeight;
|
|
// If we're using a fixedRatio then we need to set the correct ratio for this element
|
|
if (fixedRatio) {
|
|
targetWidth = targetHeight / this.getVideoRatio(elem);
|
|
}
|
|
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 > WIDTH) {
|
|
// Went over on the width, need to adjust the height proportionally
|
|
row.height = Math.floor(row.height * (WIDTH / row.width));
|
|
row.width = WIDTH;
|
|
} else if (row.width < WIDTH) {
|
|
remainingShortRows += 1;
|
|
}
|
|
totalRowHeight += row.height;
|
|
}
|
|
if (totalRowHeight < HEIGHT && remainingShortRows > 0) {
|
|
// We can grow some of the rows, we're not taking up the whole height
|
|
let remainingHeightDiff = HEIGHT - totalRowHeight;
|
|
totalRowHeight = 0;
|
|
for (let i = 0; i < rows.length; i++) {
|
|
row = rows[i];
|
|
if (row.width < WIDTH) {
|
|
// Evenly distribute the extra height between the short rows
|
|
let extraHeight = remainingHeightDiff / remainingShortRows;
|
|
if (extraHeight / row.height > (WIDTH - row.width) / row.width) {
|
|
// We can't go that big or we'll go too wide
|
|
extraHeight = Math.floor(((WIDTH - 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
|
|
y = (HEIGHT - totalRowHeight) / 2;
|
|
// Iterate through each row and place each child
|
|
for (let i = 0; i < rows.length; i++) {
|
|
row = rows[i];
|
|
// center the row
|
|
const rowMarginLeft = (WIDTH - row.width) / 2;
|
|
x = rowMarginLeft;
|
|
for (let j = 0; j < row.children.length; j++) {
|
|
const elem: HTMLVideoElement = row.children[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 / this.getVideoRatio(elem));
|
|
}
|
|
elem.style.position = 'absolute';
|
|
// $(elem).css('position', 'absolute');
|
|
const actualWidth =
|
|
targetWidth -
|
|
this.getCSSNumber(elem, 'paddingLeft') -
|
|
this.getCSSNumber(elem, 'paddingRight') -
|
|
this.getCSSNumber(elem, 'marginLeft') -
|
|
this.getCSSNumber(elem, 'marginRight') -
|
|
this.getCSSNumber(elem, 'borderLeft') -
|
|
this.getCSSNumber(elem, 'borderRight');
|
|
|
|
const actualHeight =
|
|
targetHeight -
|
|
this.getCSSNumber(elem, 'paddingTop') -
|
|
this.getCSSNumber(elem, 'paddingBottom') -
|
|
this.getCSSNumber(elem, 'marginTop') -
|
|
this.getCSSNumber(elem, 'marginBottom') -
|
|
this.getCSSNumber(elem, 'borderTop') -
|
|
this.getCSSNumber(elem, 'borderBottom');
|
|
|
|
this.positionElement(elem, x + offsetLeft, y + offsetTop, actualWidth, actualHeight, animate);
|
|
x += targetWidth;
|
|
}
|
|
y += targetHeight;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @hidden
|
|
*/
|
|
private attachElements(bigOnes: HTMLVideoElement[], normalOnes: HTMLVideoElement[], smallOnes: HTMLVideoElement[]) {
|
|
const HEIGHT =
|
|
this.getHeight(this.layoutContainer) -
|
|
this.getCSSNumber(this.layoutContainer, 'borderTop') -
|
|
this.getCSSNumber(this.layoutContainer, 'borderBottom');
|
|
const WIDTH =
|
|
this.getWidth(this.layoutContainer) -
|
|
this.getCSSNumber(this.layoutContainer, 'borderLeft') -
|
|
this.getCSSNumber(this.layoutContainer, 'borderRight');
|
|
const offsetLeft = 0;
|
|
const offsetTop = 0;
|
|
|
|
if (this.existBigAndNormalOnes(bigOnes, normalOnes, smallOnes)) {
|
|
const smallOnesAux = smallOnes.length > 0 ? smallOnes : normalOnes;
|
|
const bigOnesAux = bigOnes.length > 0 ? bigOnes : normalOnes;
|
|
this.arrangeBigAndSmallOnes(bigOnesAux, smallOnesAux);
|
|
} else if (this.onlyExistBigOnes(bigOnes, normalOnes, smallOnes)) {
|
|
// We only have one bigOne just center it
|
|
this.arrange(
|
|
bigOnes,
|
|
WIDTH,
|
|
HEIGHT,
|
|
0,
|
|
0,
|
|
this.opts.bigFixedRatio,
|
|
this.opts.bigMinRatio,
|
|
this.opts.bigMaxRatio,
|
|
this.opts.animate
|
|
);
|
|
} else if (this.existBigAndNormalAndSmallOnes(bigOnes, normalOnes, smallOnes)) {
|
|
this.arrangeBigAndSmallOnes(bigOnes, normalOnes.concat(smallOnes));
|
|
} else {
|
|
const normalOnesAux = normalOnes.concat(smallOnes);
|
|
this.arrange(
|
|
normalOnesAux,
|
|
WIDTH - offsetLeft,
|
|
HEIGHT - offsetTop,
|
|
offsetLeft,
|
|
offsetTop,
|
|
this.opts.fixedRatio,
|
|
this.opts.minRatio,
|
|
this.opts.maxRatio,
|
|
this.opts.animate
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @hidden
|
|
*/
|
|
private arrangeBigAndSmallOnes(bigOnesAux: HTMLVideoElement[], smallOnesAux: HTMLVideoElement[]) {
|
|
const HEIGHT =
|
|
this.getHeight(this.layoutContainer) -
|
|
this.getCSSNumber(this.layoutContainer, 'borderTop') -
|
|
this.getCSSNumber(this.layoutContainer, 'borderBottom');
|
|
const WIDTH =
|
|
this.getWidth(this.layoutContainer) -
|
|
this.getCSSNumber(this.layoutContainer, 'borderLeft') -
|
|
this.getCSSNumber(this.layoutContainer, 'borderRight');
|
|
const availableRatio = HEIGHT / WIDTH;
|
|
|
|
let offsetLeft = 0;
|
|
let offsetTop = 0;
|
|
let bigOffsetTop = 0;
|
|
let bigOffsetLeft = 0;
|
|
let bigWidth, bigHeight;
|
|
|
|
if (availableRatio > this.getVideoRatio(bigOnesAux[0])) {
|
|
// We are tall, going to take up the whole width and arrange small
|
|
// guys at the bottom
|
|
bigWidth = WIDTH;
|
|
bigHeight = Math.floor(HEIGHT * this.opts.bigPercentage);
|
|
offsetTop = bigHeight;
|
|
bigOffsetTop = HEIGHT - offsetTop;
|
|
} else {
|
|
// We are wide, going to take up the whole height and arrange the small
|
|
// guys on the right
|
|
bigHeight = HEIGHT;
|
|
bigWidth = Math.floor(WIDTH * this.opts.bigPercentage);
|
|
offsetLeft = bigWidth;
|
|
bigOffsetLeft = WIDTH - offsetLeft;
|
|
}
|
|
if (this.opts.bigFirst) {
|
|
this.arrange(
|
|
bigOnesAux,
|
|
bigWidth,
|
|
bigHeight,
|
|
0,
|
|
0,
|
|
this.opts.bigFixedRatio,
|
|
this.opts.bigMinRatio,
|
|
this.opts.bigMaxRatio,
|
|
this.opts.animate
|
|
);
|
|
this.arrange(
|
|
smallOnesAux,
|
|
WIDTH - offsetLeft,
|
|
HEIGHT - offsetTop,
|
|
offsetLeft,
|
|
offsetTop,
|
|
this.opts.fixedRatio,
|
|
this.opts.minRatio,
|
|
this.opts.maxRatio,
|
|
this.opts.animate
|
|
);
|
|
} else {
|
|
this.arrange(
|
|
smallOnesAux,
|
|
WIDTH - offsetLeft,
|
|
HEIGHT - offsetTop,
|
|
0,
|
|
0,
|
|
this.opts.fixedRatio,
|
|
this.opts.minRatio,
|
|
this.opts.maxRatio,
|
|
this.opts.animate
|
|
);
|
|
this.arrange(
|
|
bigOnesAux,
|
|
bigWidth,
|
|
bigHeight,
|
|
bigOffsetLeft,
|
|
bigOffsetTop,
|
|
this.opts.bigFixedRatio,
|
|
this.opts.bigMinRatio,
|
|
this.opts.bigMaxRatio,
|
|
this.opts.animate
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @hidden
|
|
*/
|
|
private existBigAndNormalOnes(bigOnes: HTMLVideoElement[], normalOnes: HTMLVideoElement[], smallOnes: HTMLVideoElement[]) {
|
|
return (
|
|
(bigOnes.length > 0 && normalOnes.length > 0 && smallOnes.length === 0) ||
|
|
(bigOnes.length > 0 && normalOnes.length === 0 && smallOnes.length > 0) ||
|
|
(bigOnes.length === 0 && normalOnes.length > 0 && smallOnes.length > 0)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @hidden
|
|
*/
|
|
private onlyExistBigOnes(bigOnes: HTMLVideoElement[], normalOnes: HTMLVideoElement[], smallOnes: HTMLVideoElement[]): boolean {
|
|
return bigOnes.length > 0 && normalOnes.length === 0 && smallOnes.length === 0;
|
|
}
|
|
|
|
/**
|
|
* @hidden
|
|
*/
|
|
private existBigAndNormalAndSmallOnes(
|
|
bigOnes: HTMLVideoElement[],
|
|
normalOnes: HTMLVideoElement[],
|
|
smallOnes: HTMLVideoElement[]
|
|
): boolean {
|
|
return bigOnes.length > 0 && normalOnes.length > 0 && smallOnes.length > 0;
|
|
}
|
|
|
|
/**
|
|
* @hidden
|
|
*/
|
|
private filterDisplayNone(element: HTMLElement) {
|
|
return element.style.display !== 'none';
|
|
}
|
|
}
|