DEV Community

loading...
Cover image for Add a text or image overlay to a video element
Daily

Add a text or image overlay to a video element

kimberleejohnson profile image Kimberlee Johnson Originally published at daily.co Updated on ・5 min read

We talk to a lot of developers building video calls at Daily, and one thing they often want to do is overlay text (like a participant’s name) or small images (muted state indicators or logos) on top of a video element. This post walks through how to do that!

Person in Daily call points to their name at the top left corner of the video stream
It took me more time to take this screenshot with one hand than it took me to add my name.

First, we’ll cover the foundational CSS for positioning one element on top of another. Then, we’ll apply that CSS and build on top of Paul’s React video chat app tutorial.

Arrange and stack elements with CSS

We’ll set position and z-index properties to arrange our elements.

position gives us control of how an element will sit in the overall layout of the page. When the property is not set, every block-level HTML element appears on a new line [0]. We don’t want that! We specifically want our name tag directly on top of and overlapping our video container. Where the name tag goes depends on the video's position.

To set up this dependent relationship, we set our video's position property to relative. Then, we can arrange any child elements, in our case our name tag, in relation to it by setting their position property to absolute.

To see this in action, experiment with removing position:relative from the .parent-container class in this codepen:

Our boxes’ top, bottom, right, and left properties offset them relative to .parent-container.

With the dependent relationship established, it's time to move on to stacking elements. To do that, we'll need the z-index property. Because we set position properties, we can make use of z-index to stack our elements. The higher the z-index number, the closer to the screen the element will be. Swap the .red-box and .green-box z-index values in the codepen to see what I mean.

We now know how to arrange child elements in relation to their parents using position, and how to stack them with z-index. We’re ready to take those concepts over to our React video chat app, but first let’s look at how we can get participant names from the Daily call object.

Passing participant names as props in React

The Daily call object keeps track of our call state, meaning important information about the meeting. This includes details like other participants (e.g. their audio and video tracks and user_name) and the things they do on the call (e.g. muting their mic or leaving)[1]. The call object also provides methods for interacting with the meeting.

In our demo app, we map the Daily call object state to a corresponding component state called callItems in callState.js. Each call item represents a participant, and contains their audio and video tracks, along with a boolean state indicator about whether or not their call is loading. To also track participant names, we'll add participantName to each call item.

const initialCallState = {
 callItems: {
   local: {
     isLoading: true,
     audioTrack: null,
     videoTrack: null,
     participantName: '',
   },
 },
 clickAllowTimeoutFired: false,
 camOrMicError: null,
 fatalError: null,
};
Enter fullscreen mode Exit fullscreen mode

We need to add participantName to our getCallItems function as well. This function loops over the call object to populate our callItems.

function getCallItems(participants, prevCallItems) {
 let callItems = { ...initialCallState.callItems }; // Ensure we *always* have a local participant
 for (const [id, participant] of Object.entries(participants)) {
   // Here we assume that a participant will join with audio/video enabled.
   // This assumption lets us show a "loading" state before we receive audio/video tracks.
   // This may not be true for all apps, but the call object doesn't yet support distinguishing
   // between cases where audio/video are missing because they're still loading or muted.
   const hasLoaded = prevCallItems[id] && !prevCallItems[id].isLoading;
   const missingTracks = !(participant.audioTrack || participant.videoTrack);
   callItems[id] = {
     isLoading: !hasLoaded && missingTracks,
     audioTrack: participant.audioTrack,
     videoTrack: participant.videoTrack,
     participantName: participant.user_name ? participant.user_name : 'Guest',
   };
   if (participant.screenVideoTrack || participant.screenAudioTrack) {
     callItems[id + '-screen'] = {
       isLoading: false,
       videoTrack: participant.screenVideoTrack,
       audioTrack: participant.screenAudioTrack,
     };
   }
 }
 return callItems;
}
Enter fullscreen mode Exit fullscreen mode

getCallItems gets called in Call.js [2]. It then passes the callItems as props via the getTiles function to <Tile>, the component that displays each participant. We’ll add participantName to the list of props:

export default function Call() {
// Lots of other things happen here! See our demo for full code.
//
function getTiles() {
   let largeTiles = [];
   let smallTiles = [];
   Object.entries(callState.callItems).forEach(([id, callItem]) => {
     const isLarge =
       isScreenShare(id) ||
       (!isLocal(id) && !containsScreenShare(callState.callItems));
     const tile = (
       <Tile
         key={id}
         videoTrack={callItem.videoTrack}
         audioTrack={callItem.audioTrack}
         isLocalPerson={isLocal(id)}
         isLarge={isLarge}
         isLoading={callItem.isLoading}
         participantName={callItem.participantName}
         onClick={
           isLocal(id)
             ? null
             : () => {
                 sendHello(id);
               }
         }
       />
     );
     if (isLarge) {
       largeTiles.push(tile);
     } else {
       smallTiles.push(tile);
     }
   });
   return [largeTiles, smallTiles];
 }

 const [largeTiles, smallTiles] = getTiles();

return (
   <div className="call">
     <div className="large-tiles">
       {
         !message
           ? largeTiles
           : null /* Avoid showing large tiles to make room for the message */
       }
     </div>
     <div className="small-tiles">{smallTiles}</div>
     {message && (
       <CallMessage
         header={message.header}
         detail={message.detail}
         isError={message.isError}
       />
     )}
   </div>
 );
}
Enter fullscreen mode Exit fullscreen mode

Now, in Tile.js, we display the name:

export default function Tile(props) {
// More code
function getParticipantName() {
   return (
     props.participantName && (
       <div className="participant-name">{props.participantName}</div>
     )
   );
 }

 return (
   <div>
     <div className={getClassNames()} onClick={props.onClick}>
       <div className="background" />
       {getLoadingComponent()}
       {getVideoComponent()}
       {getAudioComponent()}
       {getParticipantName()}
     </div>
   </div>
 );
} 
Enter fullscreen mode Exit fullscreen mode

And style it using familiar CSS in Tile.css, with our container tiles set to relative positioning and our video streams and name tags set to absolute:

.tile.small {
 width: 200px;
 margin: 0 10px;
 position: relative;
}

.tile.large {
 position: relative;
 margin: 2px;
}

.tile video {
 width: 100%;
 position: absolute;
 top: 0px;
 z-index: 1;
}

.participant-name {
 padding: 5px 5px;
 position: absolute;
 background: #ffffff;
 font-family: 'Helvetica Neue';
 font-style: normal;
 font-weight: normal;
 font-size: 1rem;
 line-height: 13px;
 text-align: center;
 color: #4a4a4a;
 top: 0;
 left: 0;
 z-index: 10;
}
Enter fullscreen mode Exit fullscreen mode

And there you have it!

If you have any questions or feedback about this post, please email me any time at kimberlee@daily.co. Or, if you’re looking to explore even more ways to customize Daily calls, explore our docs.

[0] This is not the case for inline elements.

[1] A participant’s user_name can be set in a few different ways. It can be passed as a property to the DailyIframe, or set with a meeting token.

[2] More specifically, any time there’s a change to participants on the call, Call.js dispatches an action to a reducer that updates state via getCallItems.

Discussion (0)

pic
Editor guide