/** * @internal */ export enum LayoutClass { ROOT_ELEMENT = 'OT_root', BIG_ELEMENT = 'OV_big', SMALL_ELEMENT = 'OV_small', IGNORED_ELEMENT = 'OV_ignored', SIDENAV_CONTAINER = 'sidenav-container', NO_SIZE_ELEMENT = 'no-size', CLASS_NAME = 'layout' } /** * @internal */ export enum SidenavMode { OVER = 'over', SIDE = 'side' } export enum LayoutAlignment { START = 'start', CENTER = 'center', END = 'end' } /** * @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 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 class to add to elements that should be ignored */ ignoredClass: string; /** * The maximum percentage of space the big ones should take up */ bigPercentage: any; /** * If this is set then it will scale down the big space if there is left over whitespace down to this minimum size */ minBigPercentage: number; /** * 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: boolean | 'column' | 'row'; /** * */ alignItems: LayoutAlignment; /** * */ bigAlignItems: LayoutAlignment; /** * */ smallAlignItems: LayoutAlignment; /** * The maximum width of the elements */ maxWidth: number; /** * The maximum height of the elements */ maxHeight: number; smallMaxWidth: number; smallMaxHeight: number; bigMaxWidth: number; bigMaxHeight: number; /** * If there are less elements on the last row then we can scale them up to take up more space */ scaleLastRow?: boolean; /** * Scale last row for the big elements */ bigScaleLastRow?: boolean; } /** * @internal */ export class OpenViduLayout { /** * @hidden */ private layoutContainer: HTMLElement; /** * @hidden */ private opts: OpenViduLayoutOptions; /** * Update the layout container * module export layout */ updateLayout(container: HTMLElement, opts: any) { setTimeout(() => { this.layoutContainer = container; this.opts = opts; if (this.css(this.layoutContainer, 'display') === 'none') { return; } let id = this.layoutContainer.id; if (!id) { id = 'OT_' + this.cheapUUID(); this.layoutContainer.id = id; } opts.containerHeight = this.getHeight(this.layoutContainer) - this.getCSSNumber(this.layoutContainer, 'border-top') - this.getCSSNumber(this.layoutContainer, 'border-bottom'); opts.containerWidth = this.getWidth(this.layoutContainer) - this.getCSSNumber(this.layoutContainer, 'border-left') - this.getCSSNumber(this.layoutContainer, 'border-right'); const children = Array.prototype.filter.call( this.layoutContainer.querySelectorAll(`#${id}>*:not(.${LayoutClass.IGNORED_ELEMENT})`), () => this.filterDisplayNone ); const elements = children.map((element) => { const res = this.getChildDims(element); res.big = element.classList.contains(this.opts.bigClass); return res; }); const layout = this.getLayout(opts, elements); layout.boxes.forEach((box, idx) => { const elem = children[idx]; this.css(elem, 'position', 'absolute'); const actualWidth = box.width - -this.getCSSNumber(elem, 'margin-left') - this.getCSSNumber(elem, 'margin-right') - (this.css(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.css(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); this.positionElement(elem, box.left, box.top, actualWidth, actualHeight, this.opts.animate); }); }, 50); } /** * Initialize the layout inside of the container with the options required * @param container * @param opts */ initLayoutContainer(container: HTMLElement, opts: OpenViduLayoutOptions) { // this.opts = this.defaults(opts, { // maxRatio: 3 / 2, // minRatio: 9 / 16, // fixedRatio: false, // animate: false, // bigClass: LayoutClass.BIG_ELEMENT, // smallClass: LayoutClass.SMALL_ELEMENT, // bigPercentage: 0.8, // bigFixedRatio: false, // bigMaxRatio: 3 / 2, // bigMinRatio: 9 / 16, // bigFirst: true, // alignItems: 'center', // bigAlignItems: 'center', // smallAlignItems: 'center' // }); this.opts = opts; this.layoutContainer = container; } getLayoutContainer(): HTMLElement { return this.layoutContainer; } /** * Set the layout configuration * @param options */ private setLayoutOptions(options: OpenViduLayoutOptions) { this.opts = options; } private css(el: HTMLVideoElement | HTMLElement, propertyName: any, value?: string) { if (!!value) { // We are setting one css property el.style[propertyName] = value; return NaN; } else if (typeof propertyName === 'object') { // We are setting several CSS properties at once Object.keys(propertyName).forEach((key) => { this.css(el, key, propertyName[key]); }); return NaN; } else { // We are getting the css property var computedStyle = /*(this.opts && this.opts.window) ||*/ window.getComputedStyle(el); var currentValue = computedStyle.getPropertyValue(propertyName); if (currentValue === '') { currentValue = el.style[propertyName]; } return currentValue; } } private height(el) { if (el.offsetHeight > 0) { return `${el.offsetHeight}px`; } return this.css(el, 'height'); } private width(el) { if (el.offsetWidth > 0) { return `${el.offsetWidth}px`; } return this.css(el, 'width'); } private defaults(custom: OpenViduLayoutOptions, defaults: OpenViduLayoutOptions): OpenViduLayoutOptions { var res = defaults; Object.keys(defaults).forEach((key) => { if (custom.hasOwnProperty(key)) { res[key] = custom[key]; } }); return res; } /** * @hidden */ private fixAspectRatio(elem: HTMLVideoElement, width: number) { const sub: HTMLVideoElement = elem.querySelector(`.${LayoutClass.ROOT_ELEMENT}`); 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); if (animate) { 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); } else { this.css(elem, targetPosition); if (!elem.classList.contains(LayoutClass.CLASS_NAME)) { elem.classList.add(LayoutClass.CLASS_NAME); } } this.fixAspectRatio(elem, width); } /** * @hidden */ private getChildDims(child: HTMLVideoElement): { height: number; width: number; big?: boolean; } { if (child) { if (child.videoHeight && child.videoWidth) { return { height: child.videoHeight, width: child.videoWidth }; } const video: HTMLVideoElement = child.querySelector('video'); if (video && video.videoHeight && video.videoWidth) { return { height: video.videoHeight, width: video.videoWidth }; } } return { height: 480, width: 640 }; } /** * @hidden */ private getCSSNumber(elem: HTMLElement, prop: string) { const cssStr = this.css(elem, prop); return cssStr ? parseInt(cssStr.toString(), 10) : 0; } /** * @hidden */ // Really cheap UUID function private cheapUUID() { return (Math.random() * 100000000).toFixed(0); } /** * @hidden */ private getHeight(elem: HTMLElement) { const heightStr = this.height(elem); return heightStr ? parseInt(heightStr.toString(), 10) : 0; } /** * @hidden */ private getWidth(elem: HTMLElement) { const widthStr = this.width(elem); return widthStr ? parseInt(widthStr.toString(), 10) : 0; } /** * @hidden */ // private arrange( // children: HTMLVideoElement[], // containerWidth: number, // containerHeight: number, // offsetLeft: number, // offsetTop: number, // fixedRatio: boolean, // minRatio: number, // maxRatio: number, // animate: any // ) { // const boxes = this.getLayout( // { // containerWidth, // containerHeight, // minRatio, // maxRatio, // fixedRatio, // }, // children.map((child) => this.getVideoRatio(child)) // ); // boxes.forEach((box, idx) => { // const elem = children[idx]; // this.css(elem, 'position', 'absolute'); // const actualWidth = // box.width - // 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 = // box.height - // 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, // box.left + offsetLeft, // box.top + offsetTop, // actualWidth, // actualHeight, // animate // ); // }); // } /** * @hidden */ // private attachElements( // bigOnes: HTMLVideoElement[], // normalOnes: HTMLVideoElement[], // smallOnes: HTMLVideoElement[] // ) { // const containerHeight = // this.getHeight(this.layoutContainer) - // this.getCSSNumber(this.layoutContainer, 'borderTop') - // this.getCSSNumber(this.layoutContainer, 'borderBottom'); // const containerWidth = // 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, { // containerHeight, // containerWidth, // }); // } else if (this.onlyExistBigOnes(bigOnes, normalOnes, smallOnes)) { // // We only have one bigOne just center it // this.arrange( // bigOnes, // containerWidth, // containerHeight, // 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), { // containerHeight, // containerWidth, // }); // } else { // const normalOnesAux = normalOnes.concat(smallOnes); // this.arrange( // normalOnesAux, // containerWidth - offsetLeft, // containerHeight - offsetTop, // offsetLeft, // offsetTop, // this.opts.fixedRatio, // this.opts.minRatio, // this.opts.maxRatio, // this.opts.animate // ); // } // } /** * @hidden */ // private arrangeBigAndSmallOnes( // bigOnesAux: HTMLVideoElement[], // smallOnesAux: HTMLVideoElement[], // data: { containerHeight: number; containerWidth: number } // ) { // const { containerWidth, containerHeight } = data; // let offsetLeft = 0; // let offsetTop = 0; // const availableRatio = containerHeight / containerWidth; // 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 = containerWidth; // bigHeight = Math.floor(containerHeight * this.opts.bigPercentage); // offsetTop = bigHeight; // bigOffsetTop = containerHeight - offsetTop; // } 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 * this.opts.bigPercentage); // offsetLeft = bigWidth; // bigOffsetLeft = containerWidth - 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, // containerWidth - offsetLeft, // containerHeight - offsetTop, // offsetLeft, // offsetTop, // this.opts.fixedRatio, // this.opts.minRatio, // this.opts.maxRatio, // this.opts.animate // ); // } else { // this.arrange( // smallOnesAux, // containerWidth - offsetLeft, // containerHeight - 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 this.css(element, 'display') !== 'none'; } /** * * -------------------------------------------------------------------------------- * * GET LAYOUT * * */ /** * @hidden */ private getBestDimensions( minRatio: number, maxRatio: number, width: number, height: number, count: number, maxWidth: number, maxHeight: number ) { let maxArea: number; let targetCols: number; let targetRows: number; let targetHeight: number; let targetWidth: number; let tWidth: number; let tHeight: number; let tRatio: number; // 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 tHeight = Math.floor(height / rows); tWidth = Math.floor(width / cols); 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; } } } return { maxArea, targetCols, targetRows, targetHeight, targetWidth, ratio: targetHeight / targetWidth }; } private getVideoRatio(element: { height: number; width: number; big?: boolean }) { return element.height / element.width; } private getLayout(opts: any, elements: { height: number; width: number; big?: boolean }[]) { const { maxRatio = 3 / 2, minRatio = 9 / 16, fixedRatio = false, bigPercentage = 0.8, minBigPercentage = 0, bigFixedRatio = false, bigMaxRatio = 3 / 2, bigMinRatio = 9 / 16, bigFirst = true, containerWidth = 640, containerHeight = 480, alignItems = 'center', bigAlignItems = 'center', smallAlignItems = '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; const bigIndices = []; let bigBoxes = []; let smallBoxes = []; let areas: { big: any; small: any } = { big: null, small: null }; // Move to Get Layout const smallOnes = elements.filter((element) => !element.big); const bigOnes = elements.filter((element, idx) => { if (element.big) { bigIndices.push(idx); return true; } 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); 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 }; } else { areas.small = { top: offsetTop, left: offsetLeft, width: containerWidth - offsetLeft, height: containerHeight - offsetTop }; } if (areas.big) { bigBoxes = this.getLayoutAux( { 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.small) { smallBoxes = this.getLayoutAux( { containerWidth: areas.small.width, containerHeight: areas.small.height, offsetLeft: areas.small.left, offsetTop: areas.small.top, fixedRatio, minRatio, maxRatio, alignItems: areas.big ? smallAlignItems : alignItems, maxWidth: areas.big ? smallMaxWidth : maxWidth, maxHeight: areas.big ? smallMaxHeight : maxHeight, scaleLastRow }, smallOnes ); } const boxes = []; let bigBoxesIdx = 0; let smallBoxesIdx = 0; // Rebuild the array in the right order based on where the bigIndices should be elements.forEach((element, idx) => { if (bigIndices.indexOf(idx) > -1) { boxes[idx] = bigBoxes[bigBoxesIdx]; bigBoxesIdx += 1; } else { boxes[idx] = smallBoxes[smallBoxesIdx]; smallBoxesIdx += 1; } }); return { boxes, areas }; } private getLayoutAux(opts: any, elements: { height: number; width: number; big?: boolean }[]) { const { maxRatio = 3 / 2, minRatio = 9 / 16, fixedRatio = false, containerWidth = 640, containerHeight = 480, offsetLeft = 0, offsetTop = 0, alignItems = '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] : null; 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 = []; let row; const boxes = []; // 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]; 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 '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; 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; } }