/// import { Element, Options, Box, GetLayoutRes } from 'opentok-layout-js'; const getBestDimensions = (minRatio: number, maxRatio: number, Width: number, Height: number, count: number, maxWidth: number, maxHeight: number, evenRows: boolean) => { 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 += 1) { 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 (evenRows && area === maxArea && ((cols * rows) % count) > ((targetRows * targetCols) % count)) { // We have the same area but there are more left over spots in the last row // Let's keep the previous one continue; } maxArea = area; targetHeight = tHeight; targetWidth = tWidth; targetCols = cols; targetRows = rows; } } return { maxArea, targetCols, targetRows, targetHeight, targetWidth, ratio: targetHeight / targetWidth, }; }; type Offsets = { offsetLeft: number; offsetTop: number; } type Row = { ratios: number[]; elements: Element[]; width: number; height: number; } type Areas = { small?: Box, big?: Box } const getLayout = (opts: Options & Offsets, elements: Element[]): Box[] => { const { maxRatio, minRatio, fixedRatio, containerWidth, containerHeight, offsetLeft = 0, offsetTop = 0, alignItems = 'center', maxWidth = Infinity, maxHeight = Infinity, scaleLastRow = true, evenRows = true, } = opts; const ratios = elements.map(element => element.height / element.width); const count = ratios.length; let dimensions: ReturnType; if (!fixedRatio) { dimensions = getBestDimensions(minRatio, maxRatio, containerWidth, containerHeight, count, maxWidth, maxHeight, evenRows); } else { // Use the ratio of the first video element we find to approximate const ratio = ratios.length > 0 ? ratios[0] : null; dimensions = getBestDimensions(ratio, ratio, containerWidth, containerHeight, count, maxWidth, maxHeight, evenRows); } // Loop through each stream in the container and place it inside let x = 0; let y = 0; const rows: Row[] = []; let row: Row; const boxes: Box[] = []; // 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 += 1) { if (i % dimensions.targetCols === 0) { // This is a new row row = { ratios: [], elements: [], width: 0, height: 0, }; rows.push(row); } const ratio = ratios[i]; const element = elements[i]; row.elements.push(element); 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 || element.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 += 1) { 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 += 1) { 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; } } 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 += 1) { 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 += 1) { const ratio = row.ratios[j]; const element = row.elements[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 || element.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; }; const getVideoRatio = (element: Element) => element.height / element.width; export default (opts: Options, elements: Element[]): GetLayoutRes => { 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, evenRows = true, } = opts; const availableRatio = containerHeight / containerWidth; let offsetLeft = 0; let offsetTop = 0; let bigOffsetTop = 0; let bigOffsetLeft = 0; const bigIndices: number[] = []; const bigOnes = elements.filter((element, idx) => { if (element.big) { bigIndices.push(idx); return true; } return false; }); const smallOnes = elements.filter(element => !element.big); let bigBoxes: Box[] = []; let smallBoxes: Box[] = []; const areas: Areas = {}; if (bigOnes.length > 0 && smallOnes.length > 0) { let bigWidth: number; let bigHeight: number; let showBigFirst = bigFirst === true; if (availableRatio > 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: ReturnType; if (!bigFixedRatio) { bigDimensions = getBestDimensions(bigMinRatio, bigMaxRatio, bigWidth, bigHeight, bigOnes.length, bigMaxWidth, bigMaxHeight, evenRows); } else { // Use the ratio of the first video element we find to approximate const ratio = bigOnes[0].height / bigOnes[0].width; bigDimensions = getBestDimensions(ratio, ratio, bigWidth, bigHeight, bigOnes.length, bigMaxWidth, bigMaxHeight, evenRows); } 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 smallBoxes = getLayout({ containerWidth: containerWidth, containerHeight: containerHeight - bigHeight, offsetLeft: 0, offsetTop: 0, fixedRatio, minRatio, maxRatio, alignItems: smallAlignItems, maxWidth: smallMaxWidth, maxHeight: smallMaxHeight, scaleLastRow, evenRows, }, smallOnes); let smallHeight = 0 let currentTop = undefined smallBoxes.forEach(box => { if (currentTop !== box.top) { currentTop = box.top smallHeight += box.height } }) bigHeight = Math.max(bigHeight, containerHeight - smallHeight); } 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: ReturnType; if (!bigFixedRatio) { bigDimensions = getBestDimensions(bigMinRatio, bigMaxRatio, bigWidth, bigHeight, bigOnes.length, bigMaxWidth, bigMaxHeight, evenRows); } else { // Use the ratio of the first video element we find to approximate const ratio = bigOnes[0].height / bigOnes[0].width; bigDimensions = getBestDimensions(ratio, ratio, bigWidth, bigHeight, bigOnes.length, bigMaxWidth, bigMaxHeight, evenRows); } 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 smallBoxes = getLayout({ containerWidth: containerWidth - bigWidth, containerHeight: containerHeight, offsetLeft: 0, offsetTop: 0, fixedRatio, minRatio, maxRatio, alignItems: smallAlignItems, maxWidth: smallMaxWidth, maxHeight: smallMaxHeight, scaleLastRow, evenRows, }, smallOnes); let smallWidth = 0 let currentWidth = 0 let top = 0 smallBoxes.forEach(box => { if (box.top !== top) { currentWidth = 0 top = box.top } currentWidth += box.width smallWidth = Math.max(smallWidth, currentWidth) }) bigWidth = Math.max(bigWidth, containerWidth - smallWidth); } 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 = getLayout({ 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, evenRows, }, bigOnes); } if (areas.small) { smallBoxes = getLayout({ 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, evenRows, }, smallOnes); } const boxes: Box[] = []; 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 }; };