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!
ZOOM-like video gallery with CSS Houdini 🎩
Anton Dosov ・ Jun 8 '20
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
}
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;
}
}
There is also an npm module that does just that!
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.
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 );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
I applied calculated width
and height
to .video-container
.
.video-container {
width: var(--width);
height: var(--height);
}
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 + "");
⚠️ 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));
}
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%;
}
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.
Result
This is how it looks in the real world:
Please find complete solution here. Change videos count and resize the screen to see it in action.
Open questions❓
- Is it possible to achieve a similar result without calculating video size in JavaScript? 🤔
- 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)
I posted a fun part 2, where I am building the same thing using experimental CSS Layout API from CSS Houdini 🎩. Check this out!
ZOOM-like video gallery with CSS Houdini 🎩
Anton Dosov ・ Jun 8 '20 ・ 6 min read
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
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.
Here's a gist for a zoom auto-cropper with this algo in the middle. I figured I'd do it in typescript too.