DEV Community

Cover image for Building a video gallery just like in ZOOM
Anton Dosov
Anton Dosov

Posted on • Updated on • Originally published at adosov.dev

Building a video gallery just like in ZOOM

Update

I posted a fun part 2, where I am building the same thing using experimental CSS Layout API from CSS Houdini 🎩. Check this out!

TLDR

Complete solution is here. Change videos count and resize the screen to see it in action.

Intro

Hi folks 👋

I had to build a video gallery view similar to the one ZOOM has for a video-conferencing app.

I've spent quite a bit of time trying to figure out how to build it with pure CSS and I... FAILED 😕.

Sharing my current solution with

  • a bit of JavaScript
  • CSS custom properties
  • display: flex

If someone has an idea on how to achieve similar result without using JavaScript, please share 🙏

Problem

Having videoCount videos with fixed aspectRatio and fixed container size (containerWidth, containerHeight), fit all the videos inside the container to occupy as much area as possible. Videos should have the same size and can't overflow the container.

Solution

Calculating size for a video

First I needed to make sure videos are not overflowing the container and occupying as much area as possible.

function calculateLayout(
    containerWidth: number,
    containerHeight: number,
    videoCount: number,
    aspectRatio: number
  ): { width: number; height: number, cols: number } {
 // see implementation in codesandbox
}
Enter fullscreen mode Exit fullscreen mode

Current implementation brute-force searches the layout which occupies the most of the available space. It compares the sum of video areas for every possible number of columns.

// pseudocode, see codesandbox for complete version
let bestArea;
for (let cols = 1; cols <= videoCount; cols++) {
   const currentArea = /* sum of video areas in this layout */
   if (bestArea < currentArea) {
      bestArea = currentArea;
   }
}
Enter fullscreen mode Exit fullscreen mode

There is also an npm module that does just that!

GitHub logo fzembow / rect-scaler

Find largest rectangle and square sizes when fitting them into a container

rect-scaler

A set of javascript functions for calculating how large a set of equally sized squares or rectangles can be to fit within an arbitrary rectangular container, to cover it as fully as possible.

Illustration

Useful for graphical layouts where you need to space items in a nice way. This algorithm does not allow for rotations, and is not generic bin packing.

Usage

Install from npm:

npm install rect-scaler

Fitting squares

Pass the size of the container and the number of squares that need to be placed to largestSquare(), resulting in an object containing the optimal solution.

import { largestSquare } from "rect-scaler";

const containerWidth = 100;
const containerHeight = 100;
const numSquares = 8;
const { rows, cols, width, height, area } = largestSquare(
  containerWidth,
  containerHeight,
  numSquares
);
Enter fullscreen mode Exit fullscreen mode

Fitting rectangles

Pass the size of the container and…

But is there a better way then brute-force? Is it worth it if we assume the maximum videoCount is 50? 🤔

Markup, styles & CSS custom properties

The HTML structure I went with:

<body>
    <div id="gallery">
      <div class="video-container">
        <video></video>
      </div>
      <div class="video-container">
        <video></video>
      </div>
    </div>
</body
Enter fullscreen mode Exit fullscreen mode

I applied calculated width and height to .video-container.

.video-container {
  width: var(--width);
  height: var(--height);
}
Enter fullscreen mode Exit fullscreen mode

I used CSS custom properties to pass values calculated in JavaScript.

const gallery = document.getElementById('gallery');
gallery.style.setProperty("--width", width + "px");
gallery.style.setProperty("--height", height + "px");
gallery.style.setProperty("--cols", cols + "");
Enter fullscreen mode Exit fullscreen mode

⚠️ Don't forget to recalculate these values when the screen size or number of videos changes.

Then I used display: flex to layout .video-container elements

#gallery {
  display: flex;
  justify-content: center;
  flex-wrap: wrap;
  max-width: calc(var(--width) * var(--cols));
}
Enter fullscreen mode Exit fullscreen mode

As .video-container sizes are calculated to fit into the container, I didn't have to worry about anything else here.
I also found justify-content: center; to work best for the use case, as it nicely centers not fully filled rows.
The purpose of max-width is to force line breaks.

Handling different aspect ratios

This gallery has a constraint of a fixed aspect ratio for all elements. But videos could have different ratios. This is where having .video-container wrapping <video/> is handy.

.video-container {
  width: var(--width);
  height: var(--height);
}

video {
  height: 100%;
  width: 100%;
}
Enter fullscreen mode Exit fullscreen mode

This way video occupies as much space as possible within its container and preserves its original aspect ratio.

For example, changing the ratio of a container from 16:9 to 1:1 doesn't distort videos with the original 16:9 ratio.
Different aspect ratio

Result

This is how it looks in the real world:
Screenshot from Meetter with grid layout

Please find complete solution here. Change videos count and resize the screen to see it in action.

Open questions❓

  1. Is it possible to achieve a similar result without calculating video size in JavaScript? 🤔
  2. Is there a better way of calculating video sizes than brute-force search? Is it worth it if a number of videos can't exceed 50? 🤔

Top comments (4)

Collapse
 
antondosov profile image
Anton Dosov

I posted a fun part 2, where I am building the same thing using experimental CSS Layout API from CSS Houdini 🎩. Check this out!

Collapse
 
tanimmirza profile image
Tanim

Hello thank you for sharing this. Very helpful! A question, if I have videos with different aspect ratios inside one video conference, How can I make sure that all fits within the video-container box? I tried your solution and it works perfect if one person is on pc browser and other user is on iPhone X (landscape mode) but when the other user is on portrait mode, the video goes out of the box. Btw I am an amateur developer, so i apologize in advance if this is a silly question. Thanks

Collapse
 
bbernstein profile image
Bernie Bernstein

I just used this as part of some code to auto-crop zoom galleries to make it easier to stream to OBS. Thanks for figuring out the algorithm.

Collapse
 
bbernstein profile image
Bernie Bernstein

Here's a gist for a zoom auto-cropper with this algo in the middle. I figured I'd do it in typescript too.