The Video Display Calculator lets you take over how participant videos are arranged. Instead of using our layout, you supply a function that receives the container size and the participants, and returns an absolute position for every video.
It is part of the JavaScript API, so everything on that page about getting the handler applies here too.
| Term | Meaning |
|---|---|
| Video | The video stream of one participant |
| Container | The area of the meeting room in which all videos are displayed |
| Video container | One of the 12 slots inside the container. Each holds one video. |
If you are using an AI coding assistant, copy the text below into it and add a sentence describing what you want to build. AI assistants accessing this page receive a separate, token-optimized version of our full developer documentation, specifically tailored by our engineers for AI coding assistants.
Before writing any code, read this page:
https://www.veeting.com/en/developer-documentation/video-display-calculator
It documents how to take over video layout in a Veeting Rooms meeting
room. Keep these five rules in mind:
1. videoPositions and hiddenContainers are matched by index. A
position whose hiddenContainers entry is still true is NOT shown.
Set it to false for every container you fill.
2. There are 12 containers. Do not return more.
3. The self view is in no participant array. If hasSelfView is true
and I want it shown, add a position for participantId "myself".
4. The function must return synchronously. No promises, no await.
5. If it throws, the room silently falls back to its own layout, so
add error handling I can actually see.
What I want to build:The presentation layout splits the container into two areas. The classic layout has no secondary area: the primary area always fills the container.
By default, every video sits in the primary area, which covers the whole container. A moderator can move videos into the secondary area; as soon as one video is there, the room splits the container.

The secondary area can sit against any edge, and it comes in two sizes:
| Parameter | Meaning |
|---|---|
secondaryPosition | none, left, top, right, or bottom |
secondarySize | 0.15 for the small area, 0.5 for the large one, as a fraction of the container |
You receive both values. Whether you honor them, and how you divide the space, is up to you: you get pixel dimensions for the whole container, not for each area.
The meeting room calls your function whenever it has to recalculate positions: when somebody joins or leaves, when the container is resized, and when the moderator rearranges the room. It must return synchronously.
The meeting room has 12 video containers. You decide which ones you use and which stay hidden. Register your calculator with setVideoDisplayCalculator, as at the end of the example below.
The parameters are positional, so their names are yours to choose. They arrive in this order:
| Order | Parameter | Type | Meaning |
|---|---|---|---|
| 1 | containerWidth | number | Width of the container, in pixels |
| 2 | containerHeight | number | Height of the container, in pixels |
| 3 | hasSecondaryArea | boolean | Whether the secondary area is visible |
| 4 | secondaryPosition | SecondaryVideoPosition | Which edge the secondary area sits against |
| 5 | secondarySize | SecondaryVideoSize | 0.15 or 0.5 |
| 6 | primaryParticipants | string[] | Participant IDs in the primary area |
| 7 | secondaryParticipants | string[] | Participant IDs in the secondary area |
| 8 | participantsOrder | string[] | All participant IDs, in display order |
| 9 | participantsMediaInformation | IParticipantsMediaInformation | Media state and video dimensions per participant |
| 10 | hasSelfView | boolean | Whether a self view is present |
| 11 | isSelfviewInSecondary | boolean | Whether the self view belongs in the secondary area |
| 12 | isTVMode | boolean | Whether the room is in TV mode |
The self view is not in any participant array. If
hasSelfViewistrueand you want to show it, add a position for the participant IDmyself. Nothing else adds it for you.
| Property | Type | Meaning |
|---|---|---|
| videoPositions | IVideoPosition[] | One entry per video you want displayed |
| hiddenContainers | boolean[] | 12 entries. false shows the video container at that index; true hides it. |
The two arrays are matched by index: the first entry of videoPositions applies to video container 0, and so on. A position whose hiddenContainers[i] is left at true will not appear. That is the most common mistake on this page.
An IVideoPosition looks like this:
| Property | Type | Meaning |
|---|---|---|
| participantId | string | Whose video goes in this video container, or myself for the self view |
| width | number | Width in pixels |
| height | number | Height in pixels |
| top | number | Pixels from the top of the container |
| left | number | Pixels from the left of the container |
| zIndex | number | Stacking order, if you overlap videos |
| cssClasses | string[] | Optional. Every class must start with video-container-. |
If your calculator throws, the meeting room falls back to its built-in layout and logs the error to the browser console. Nothing appears in the meeting UI, so if your layout does not show up, open the console and look for "Error in custom video display calculator". Returning nothing at all falls back the same way but logs nothing, so an empty console does not mean your calculator ran.
This places the first participant in the center, the others in a row above, and the self view below.

The example uses only the primary area.
const videoDisplayCalculator = {
calculatePositions: (containerWidth,
containerHeight,
hasSecondaryArea,
secondaryPosition,
secondarySize,
primaryParticipants,
secondaryParticipants,
participantsOrder,
participantsMediaInformation,
hasSelfView,
isSelfviewInSecondary,
isTVMode) => {
const result = {
videoPositions: [],
hiddenContainers: Array(12).fill(true)
};
// We want to display the videos with a 4:3 format
const threeToFour = 3 / 4;
// The main video is placed in the center, 65% of the container size
const mainContainerSizePercentage = 0.65;
let mainContainerWidth = 0;
let mainContainerHeight = 0;
let mainContainerTop = 0;
let mainContainerLeft = 0;
if (Array.isArray(participantsOrder) && participantsOrder.length > 0) {
mainContainerWidth = containerWidth * mainContainerSizePercentage;
mainContainerHeight = mainContainerWidth * threeToFour;
if (mainContainerHeight > containerHeight * mainContainerSizePercentage) {
mainContainerHeight = containerHeight * mainContainerSizePercentage;
mainContainerWidth = mainContainerHeight / threeToFour;
}
mainContainerTop = (containerHeight - mainContainerHeight) / 2;
mainContainerLeft = (containerWidth - mainContainerWidth) / 2;
// Adding the main video
result.videoPositions.push({
participantId: participantsOrder[0],
width: mainContainerWidth,
height: mainContainerHeight,
top: mainContainerTop,
left: mainContainerLeft,
zIndex: 1
});
result.hiddenContainers[0] = false;
const numberOfOtherVideos = participantsOrder.length - 1;
if (numberOfOtherVideos > 0) {
// Adding all other videos in the top row
let otherContainersWidth = containerWidth / numberOfOtherVideos;
let otherContainersHeight = otherContainersWidth * threeToFour;
if (otherContainersHeight > mainContainerTop) {
otherContainersHeight = mainContainerTop;
otherContainersWidth = otherContainersHeight / threeToFour;
}
const otherContainersTop = (mainContainerTop - otherContainersHeight) / 2;
let otherContainersLeft = (containerWidth / 2)
- ((otherContainersWidth * numberOfOtherVideos) / 2);
for (let i = 1; i < participantsOrder.length; i++) {
result.videoPositions.push({
participantId: participantsOrder[i],
width: otherContainersWidth,
height: otherContainersHeight,
top: otherContainersTop,
left: otherContainersLeft,
zIndex: 1
});
result.hiddenContainers[i] = false;
otherContainersLeft += otherContainersWidth;
}
}
}
if (hasSelfView) {
// The self view is never in participantsOrder, so add it explicitly.
const selfViewHeight = containerHeight - mainContainerTop - mainContainerHeight;
const selfViewWidth = selfViewHeight / threeToFour;
result.videoPositions.push({
participantId: "myself",
width: selfViewWidth,
height: selfViewHeight,
top: containerHeight - selfViewHeight,
left: (containerWidth / 2) - (selfViewWidth / 2),
zIndex: 1
});
// Reveal the container this position was pushed into. Without this the
// self view is calculated correctly and then never shown.
result.hiddenContainers[result.videoPositions.length - 1] = false;
}
return result;
}
};
window["wlvmrApiReady"] = (handler) => {
if (!window[handler]) {
console.error(`Handler window.${handler} not found!`);
return;
}
window[handler].setVideoDisplayCalculator(videoDisplayCalculator);
};type SecondaryVideoPosition = "none" | "left" | "top" | "right" | "bottom";
enum SecondaryVideoSize {
small = 0.15,
large = 0.5
}
interface IVideoPosition {
participantId: string;
width: number;
height: number;
top: number;
left: number;
zIndex: number;
cssClasses?: string[];
}
interface IVideoDisplayConfig {
videoPositions: IVideoPosition[];
hiddenContainers: boolean[];
}
interface IMediaInformation {
audioMuted?: boolean;
videoMuted?: boolean;
hasAudio?: boolean;
hadAudio?: boolean;
hasVideo?: boolean;
// Bad connectivity sometimes stops the video: hasVideo is false and
// hadVideo is true, which means we expect it to come back.
hadVideo?: boolean;
videoWidth?: number;
videoHeight?: number;
}
interface IParticipantsMediaInformation {
[participantId: string]: IMediaInformation;
}hiddenContainers entry at true, so the video never appears.myself.video-container-.secondarySize is always 0.15. It is 0.5 for the large secondary area.Contact our team to discuss the details.