ParticipantView customizations

In this guide, you’ll learn how to customize default call layouts and how to apply custom video placeholder and UI components to the ParticipantView component. We recommend using the ParticipantView component provided by the SDK because it handles rendering your video. It also manages video quality updates, subscriptions, and visibility state when the element is not in the visible viewport. By following this guide, you’ll be able to tailor the call layouts and visual elements according to your specific needs while leveraging the capabilities provided by the ParticipantView component.

ParticipantViewUI and VideoPlaceholder templates

ParticipantView comes by default with certain participant information displayed in the “box” - such as mute states of either video or audio, quality indicator, participant’s name, reactions, etc. which are displayed over the video element positioned absolutely (see picture). By utilizing the ParticipantViewUI property, we can extend the ParticipantView component with custom UI elements specific to each participant. This allows us to tailor the visual representation of the participant based on our application’s requirements and design.

ParticipantViewUI elements

The VideoPlaceholder property allows us to replace the default placeholder displayed over the video element when the video is not playing. This placeholder is positioned absolutely over the video element. It provides us with the flexibility to customize the placeholder’s appearance and content according to our needs.

Our custom VideoPlaceholder component should be able to accept a style property coming from ParticipantView to allow for absolute positioning.

Customization in SDK provided layout components (preferred way)

We highly recommend using layout components provided by the SDK as it is possible to modify the ParticipantViewUI in these call layouts. You can follow this guide to build your own call layout.

PaginatedGridLayout

Call layout for large calls where ParticipantView elements are equally sized, you have the ability to customize the ParticipantViewUI and VideoPlaceholder components using their respective properties of this component.

import {
  PaginatedGridLayout,
  StreamVideo,
  StreamCall,
  useParticipantViewContext,
  type VideoPlaceholderProps,
} from '@stream-io/video-react-sdk';

const CustomParticipantViewUI = () => {
  const { participant } = useParticipantViewContext();

  return (
    <div className="participant-name">{participant.name || participant.id}</div>
  );
};

const CustomVideoPlaceholder = ({ style }: VideoPlaceholderProps) => {
  const { participant } = useParticipantViewContext();

  return (
    <div className="video-placeholder" style={style}>
      <img src={participant.image} alt={participant.id} />
    </div>
  );
};

const App = ({ client, callId }) => {
  return (
    <StreamVideo client={client}>
      <StreamCall callId={callId}>
        <PaginatedGridLayout
          VideoPlaceholder={CustomVideoPlaceholder}
          ParticipantViewUI={CustomParticipantViewUI}
        />
      </StreamCall>
    </StreamVideo>
  );
};

SpeakerLayout

The SpeakerLayout component allows you to tweak participant UI in the spotlight and in the scrollable bar independently.

import {
  SpeakerLayout,
  StreamVideo,
  StreamCall,
  useParticipantViewContext,
  type VideoPlaceholderProps,
} from '@stream-io/video-react-sdk';

const CustomParticipantViewUIBar = () => {
  const { participant } = useParticipantViewContext();

  return (
    <div className="bar-participant-name">
      {participant.name || participant.id}
    </div>
  );
};

const CustomParticipantViewUISpotlight = () => {
  const { participant } = useParticipantViewContext();

  return (
    <div className="spotlight-participant-name">
      {participant.name || participant.id}
    </div>
  );
};

const CustomVideoPlaceholder = ({ style }: VideoPlaceholderProps) => {
  const { participant } = useParticipantViewContext();

  return (
    <div className="video-placeholder" style={style}>
      <img src={participant.image} alt={participant.id} />
    </div>
  );
};

const App = ({ client, callId }) => {
  return (
    <StreamVideo client={client}>
      <StreamCall callId={callId}>
        <SpeakerLayout
          VideoPlaceholder={CustomVideoPlaceholder}
          ParticipantViewUIBar={CustomParticipantViewUIBar}
          ParticipantViewUISpotlight={CustomParticipantViewUISpotlight}
        />
      </StreamCall>
    </StreamVideo>
  );
};

Non-mirrored video

By default, local participant video (video feed from the user’s own camera) is mirrored, since people are generally more accustomed to seeing themselves that way. It’s possible to disable this behavior using the mirrorLocalParticipantVideo prop, which is supported by all built-in layout components:

<SpeakerLayout mirrorLocalParticipantVideo={false} />
// or
<PaginatedGridLayout mirrorLocalParticipantVideo={false} />

Standalone ParticipantView customization (in custom call layouts)

The ParticipantViewUI property accepts three possible values:

  • ReactElement - find out what’s considered a ReactElement in the React documentation
  • component which can access ParticipantView related data through useParticipantViewContext hook
  • null if you wish to not render any UI

The following example shows the utilization of the ParticipantViewUI prop by passing down ReactElement:

import { type PropsWithChildren } from 'react';
import {
  type VideoPlaceholderProps,
  ParticipantView,
  useCallStateHooks,
  useParticipantViewContext,
} from '@stream-io/video-react-sdk';

const CustomVideoPlaceholder = ({ style }: VideoPlaceholderProps) => {
  const { participant } = useParticipantViewContext();

  return (
    <div className="video-placeholder" style={style}>
      <img src={participant.image} alt={participant.id} />
    </div>
  );
};

const CustomParticipantViewUI = ({ children }: PropsWithChildren) => {
  return <div className="participant-name">{children}</div>;
};

const CustomCallLayout = () => {
  const { useParticipants } = useCallStateHooks();
  const participants = useParticipants();

  return (
    <div className="custom-call-layout">
      {participants.map((participant) => (
        <ParticipantView
          key={participant.sessionId}
          VideoPlaceholder={CustomVideoPlaceholder}
          ParticipantViewUI={
            <CustomParticipantViewUI>
              {participant.name || participant.id}
            </CustomParticipantViewUI>
          }
        />
      ))}
    </div>
  );
};
© Getstream.io, Inc. All Rights Reserved.