Video display calculator

Overview

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.

TermMeaning
VideoThe video stream of one participant
ContainerThe area of the meeting room in which all videos are displayed

Using this page with an AI assistant

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 primary and secondary areas

The presentation layout splits the container into two areas. The classic layout has no secondary area, and the primary one 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 container is split.

Primary and secondary area

The secondary area can sit against any edge, and it comes in two sizes:

ValueMeaning
secondaryPositionnone, left, top, right, or bottom
secondarySize0.15 for the small area, 0.5 for the large one, as a fraction of the container

You receive both values. Honoring them is up to you, and so is the split: you get raw pixel dimensions for the whole container, not for each area.

Writing the calculator

Your function is called whenever positions need to be recalculated: on join, on leave, on resize, and when the moderator rearranges the room. It must return synchronously.

The meeting room has 12 video containers. You decide which are used and which stay hidden.

What you receive

The parameters are positional, so their names are yours to choose. They arrive in this order:

OrderParameterTypeMeaning
1containerWidthnumberWidth of the video container, in pixels
2containerHeightnumberHeight of the video container, in pixels
3hasSecondaryAreabooleanWhether the secondary area is visible
4secondaryPositionSecondaryVideoPositionWhich edge the secondary area sits against
5secondarySizeSecondaryVideoSize0.15 or 0.5
6primaryParticipantsstring[]Participant IDs in the primary area
7secondaryParticipantsstring[]Participant IDs in the secondary area
8participantsOrderstring[]All participant IDs, in display order
9participantsMediaInformationIParticipantsMediaInformationMedia state and video dimensions per participant
10hasSelfViewbooleanWhether a self view is present
11isSelfviewInSecondarybooleanWhether the self view belongs in the secondary area
12isTVModebooleanWhether the room is in TV mode

The self view is not in any participant array. If hasSelfView is true and you want to show it, add a position for the participant ID myself. Nothing else adds it for you.

What you return

PropertyTypeMeaning
videoPositionsIVideoPosition[]One entry per video you want displayed
hiddenContainersboolean[]12 entries. false shows the container at that index; true hides it.

The two arrays are matched by index: the first entry of videoPositions applies to container 0, and so on. A position with hiddenContainers[i] left at true will not appear, which is the single most common mistake here.

An IVideoPosition is:

PropertyTypeMeaning
participantIdstringWhose video goes in this container, or myself for the self view
widthnumberWidth in pixels
heightnumberHeight in pixels
topnumberPixels from the top of the container
leftnumberPixels from the left of the container
zIndexnumberStacking order, if you overlap videos
cssClassesstring[]Optional. Every class must start with video-container-.

If your calculator fails

If your calculator throws or returns an empty object, the meeting room silently falls back to its built-in layout. Nobody sees an error, so if your layout does not appear, check the browser console first.

Example

This places the first participant in the center, the others in a row above, and the self view below.

Example video display

It 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 definitions

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;
}

Common mistakes

  • Pushing a position but leaving its hiddenContainers entry at true, so the video never appears.
  • Forgetting the self view, which is in no participant array and must be added as myself.
  • Expecting an error when the calculator throws. The room falls back silently to the built-in layout.
  • Returning more than 12 positions.
  • Using a CSS class that does not start with video-container-.
  • Assuming secondarySize is always 0.15. It is 0.5 for the large secondary area.
  • Doing asynchronous work. The function must return a result synchronously.

Not sure how to best implement your project?

Contact our team to discuss the details.