Share admin privileges with participants during a real-time video call

Having someone to oversee a video call meeting is important for numerous reasons, including being able to manage which participants are allowed in the call. With Daily’s Client SDK for JavaScript, participants can optionally join as meeting owners, which gives them special privileges like being able to remove participants from a call.

In some cases, it can be useful for meeting owners to share their administrator responsibilities with other participants, too.

In today’s post, we’ll look at sample code that demonstrates how to let meeting owners promote participants to admins, as well as how to let admins or owners remove other participants.

Daily meeting owners vs. admins

To start, let’s look at how meeting owners and admins differ.

Meeting owner privileges

A meeting owner is a participant with the most privileges related to managing room access.

Participants can join as a meeting owner by using a meeting owner token: a token that has the is_owner property set to true.

Being a meeting owner allows the participant to:

  • Share media (audio/video/screen) in owner-only broadcast mode, if these features are enabled in the Daily room.
  • Start or stop a live stream, if live streaming is enabled in Daily the room.
  • Start or stop call transcription.
  • Allow participants to join a private room by responding to knocking.
  • Update other participants, such as adding or revoking admin permissions and muting their devices.

(See an example of creating a token using in a fetch request in today’s sample app.)

Admins

Meeting admins are similar to meeting owners, with two key differences:

  • A meeting owner has to join the call with an owner meeting token. An admin can join with an admin token or be promoted to an admin mid-call.
  • Unlike an owner, an admin can be demoted and lose admin privileges.
  • An owner has all relevant privileges within the call, whereas an admin can be given more granular permissions. Depending on the specific permissions provided, admins can potentially do any of the actions listed above for meeting owners; however, they cannot remove a meeting owner from a call or remove their meeting owner privileges.
Table showing difference between owners and admins

Now, onto the code!

Today’s goals

In this tutorial, we’ll look at sample code for how to build an admin panel to let meeting owners upgrade regular participants to admins or remove them from the call. We’ll use a sample app built with Next.js and Daily Prebuilt.

The meeting owner’s view of the demo, with buttons to remove a participant or make them an admin.
The meeting owner’s view of the demo, with buttons to remove a participant or make them an admin.

This tutorial will focus on two features:

  • A button that converts a participant to an admin.
  • A button that removes the participant (or admin) from the call.

We won’t cover some of the more general Daily-related code like rendering Daily Prebuilt in your app, but we’ll include some related blog posts at the end!

To test the demo app yourself, follow the instructions included in its README.

Project structure

The Home component’s default view, which renders the Header and JoinForm components.
The Home component’s default view, which renders the Header and JoinForm components.

When the main route of our app (e.g., localhost:3000/) is visited, the component in the page.js file found at the top-level of the app’s /app directory is rendered – in this case, the Home component, which parents all other components in the app.

Illustration depicting Home, Header, DailyContainer, JoinForm, Admin Panel, and containerRef components
Component structure for this demo app.

The DailyContainer component is the home of our video call feature and has a number of conditionally-rendered components/elements:

  • A form (JoinForm) to join a call.
  • The AdminPanel component to upgrade regular participants to admins or remove them from the call.
  • The containerRef, a div which will contain the Daily Prebuilt UI once the JoinForm is submitted and the call is created.
The admin panel is highlighted in red and the Daily Prebuilt container is highlighted in blue.
The admin panel is highlighted in red and the Daily Prebuilt container is highlighted in blue.

Creating a Daily room and joining the call

Submitting the JoinForm form to create and join a call.
Submitting the JoinForm form to create and join a call.

Let’s start by quickly familiarlizing ourselves with what happens after the JoinForm is submitted.

The JoinForm itself is conditionally rendered and shown by default. Once it’s submitted, it’s destroyed and the call-related components are displayed instead.

Default view of the JoinForm.
Default view of the JoinForm.

There are also two conditions to be aware of when the JoinForm is rendered:

  1. (Default) The user is creating a new Daily room to join and will join as a meeting owner with an owner meeting token. They can then share a link to that specific room.
  2. If the participant is using a shared link with a url query param (e.g., http://localhost:3000/?url=https://domain.daily.co/[room-name]), a “Daily room URL” form input will be rendered to indicate which room is being joined and no new room or meeting token will be created for them.
Form showing name and Daily room URL fields
Joining a specific Daily room.

This means there are two types of participants on join: meeting owners and regular participants. (No one joins as an admin!)

When the JoinForm is submitted, a few things happen.

If the participant is joining as a meeting owner:

  • A new room is created.
  • An owner meeting token is created.
  • An instance of a call frame (or DailyIframe class) is created. (Note: This is called callFrame in the code samples below.)
  • The call is joined using the meeting token.

If the participant is joining an existing call, the call frame is created and joined without a meeting token.

Now that we know how our participants can join the video call, let’s see how the AdminPanel component works.

Rendering the AdminPanel component

The AdminPanel component will render a list of all call participants. If the local participant has owner or admin privileges, each participant in the list will have buttons to remove that participant or promote them to be an admin. (Admins can’t remove owners, though!)

Owner view of a call with two participants.
Owner view of a call with two participants.

Before getting into how we remove/promote participants, let’s first look at how we keep track of them in app state. In DailyContainer, there’s a participants object saved in the component’s state:

   const [participants, setParticipants] = useState({});

Any time someone joins the call, we update the participants object by adding the participant’s session ID as the key and the participant’s object as the value.

   setParticipants((p) => ({
      ...p,
      [e.participant.session_id]: e.participant,
    }));

Note: When someone joins as an owner or anyone is promoted to an admin, their status is tracked in DailyContainer’s state. These values are passed as props to AdminPanel, too.

If someone leaves the call, we delete their item from the object:

    setParticipants((p) => {
      const currentParticipants = { ...p };
      delete currentParticipants[e.participant.session_id];
      return currentParticipants;
  });

Using participants, we can render all call participants as a list. First we render the AdminPanel component itself, including number of props:

// DailyContainer.js
{callFrame && (
        <>
          <AdminPanel
            participants={participants}
            localIsOwner={isOwner}
            localIsAdmin={isAdmin}
            makeAdmin={makeAdmin}
            removeFromCall={removeFromCall}
          />
          // … See source code
        </>
      )}

The AdminPanel component is rendered for all participants, but only owners and admins will see buttons to update other participants:

An owner’s view of a two-person call where the second person is a regular participant.
An owner’s view of a two-person call where the second person is a regular participant.

A regular participant will instead see only the participant information:

Participant list where no participant can be promoted.
A regular participant’s view of the AdminPanel.

The main functionality of AdminPanel is actually defined in its child component, ParticipantListItem:

export default function AdminPanel({
  participants,
  makeAdmin,
  removeFromCall,
  localIsOwner,
  localIsAdmin,
}) {
  return (
    <div className='admin-panel'>
      // … See source code

      <ul>
        {Object.values(participants).map((p, i) => {
          const handleMakeAdmin = () => makeAdmin(p.session_id);
          const handleRemoveFromCall = () => removeFromCall(p.session_id);
          return (
            <ParticipantListItem
              count={i + 1} // for numbered list
              key={p.session_id}
              p={p}
              localIsOwner={localIsOwner}
              localIsAdmin={localIsAdmin}
              makeAdmin={handleMakeAdmin}
              removeFromCall={handleRemoveFromCall}
            />
          );
        })}
      </ul>
    </div>
  );
}

Using the participants prop, a ParticipantListItem component is rendered for each participant in an unordered list (ul).

A list of two ParticipantListItem components with the second one highlighted.

ParticipantListItem is passed most of the props AdminPanel received and then actually uses them:

const ParticipantListItem = ({
  p,
  makeAdmin,
  removeFromCall,
  localIsOwner,
  localIsAdmin,
  count,
}) => (
  <li>
    <span>
      {`${count}. `}
      {p.permissions.canAdmin && <b>{p.owner ? 'Owner | ' : 'Admin | '}</b>}
      <b>{p.local && '(You) '}</b>
      {p.user_name}: {p.session_id}
    </span>{' '}
    {!p.local && !p.owner && (localIsAdmin || localIsOwner) && (
      <span className='buttons'>
        {(!p.permissions.canAdmin || localIsOwner) && (
          <button className='red-button-secondary' onClick={removeFromCall}>
            Remove from call
          </button>
        )}
        {!p.permissions.canAdmin && (
          <button onClick={makeAdmin}>Make admin</button>
        )}
      </span>
    )}
  </li>
);

Here, we:

  • Add the participant’s number to their line item.
  • Indicate if they’re an owner or admin and if it’s the local participant (you!).
  • Render their username and session ID to confirm they’re unique participants.
  • Conditionally render two buttons to either remove the participant or make them an admin.

Next, let’s focus on the button to make a participant an admin.

Converting non-admins to admins

As we saw above, the ParticipantListItem component conditionally renders a button to let owners or admins make a regular participant an admin.

The “Make admin” button click handler uses the makeAdmin prop passed down from DailyContainer and includes the participant’s session_id so we known which participant is being upgraded to an admin:

// AdminPanel.js

return (
        // … See See source code for full code block
        {Object.values(participants).map((p, i) => {
          const handleMakeAdmin = () => makeAdmin(p.session_id);
          const handleRemoveFromCall = () => removeFromCall(p.session_id);

          return (
            <ParticipantListItem
makeAdmin={handleMakeAdmin}
	//… See source code

On click, the button invokes the makeAdmin() function declared in DailyContainer, which will upgrade the participant to an admin via Daily’s updateParticipant() instance method:

  const makeAdmin = useCallback(
    (participantId) => {
      // https://docs.daily.co/reference/daily-js/instance-methods/update-participant#permissions
      callFrame.updateParticipant(participantId, {
        updatePermissions: {
          canAdmin: true,
        },
      });
    },
    [callFrame]
  );

updateParticipant() can be used for various participant updates and will act differently depending on the options passed in the second parameter. In this case, we want to make the participant an admin so we set the canAdmin property to true.

Using updateParticipant() this way will in fact make the participant an admin, but it won’t update our UI. To do this, we need to listen for the ”participant-updated” Daily event, which is emitted any time a participant is updated in any way.

Updating the participant list UI for new admins

To see how we update the UI for new admins, we need to backtrack for a second.

When the call frame was first created, a series of Daily event listeners were attached to it:

  const addDailyEvents = (dailyCallFrame) => {
    // https://docs.daily.co/reference/daily-js/instance-methods/on
    dailyCallFrame
      .on('joined-meeting', handleJoinedMeeting)
      .on('participant-joined', handleParticipantJoined)
      .on('participant-updated', handleParticipantUpdate)
      // … See source code

The handleParticipantUpdate() method is attached as the handler for ”participant-updated”:

  const handleParticipantUpdate = (e) => {
    const { participant } = e;
    const id = participant.session_id;
    if (!prevParticipants.current[id]) return;
    // Only update the participants list if the permission has changed.

    if (
      prevParticipants.current[id].permissions.canAdmin !==
      participant.permissions.canAdmin
    ) {
      setParticipants((p) => ({
        ...p,
        [id]: participant,
      }));
      if (participant.local) {
        setIsAdmin(participant.permissions.canAdmin);
      }
    }
  };

For the purposes of this app, we are only looking for changes to the participant.permissions.canAdmin status. If the previously saved value is different from the value included in the event’s payload for the participant, we update the participants list.

Note: prevParticipants is a ref that keeps a copy of the participants list so that we can compare the current list with possible updates.

Once the participant change is updated in state, the AdminPanel component will automatically receive the updates through the participants prop and update how the panel is rendered.

A participant being made an admin and the admin panel updating its appearance in response.
A participant being made an admin.

Ejecting a participant from the call

Ejecting (or removing) a participant from the call works almost identically from a code perspective.

In AdminPanel when the ParticipantListItems are rendered, the removeFromCall() prop is passed down, as well:

// AdminPanel

return (
        // … See source code
        {Object.values(participants).map((p, i) => {
          const handleMakeAdmin = () => makeAdmin(p.session_id);
          const handleRemoveFromCall = () => removeFromCall(p.session_id);

          return (
            <ParticipantListItem
removeFromCall={handleRemoveFromCall}
	//… See source code

This is then used by the button element in ParticipantListItem to remove a participant.

        {(!p.permissions.canAdmin || localIsOwner) && (
          <button className='red-button-secondary' onClick={removeFromCall}>
            Remove from call
          </button>
        )}

The actual action it triggers is defined in DailyContainer, which will invoke the updateParticipant() instance method:

  const removeFromCall = useCallback(
    (participantId) => {
      // https://docs.daily.co/reference/daily-js/instance-methods/update-participant#setaudio-setvideo-and-eject
      callFrame.updateParticipant(participantId, {
        eject: true,
      });
    },
    [callFrame]
  );

The main difference here is the properties passed to updateParticipant(); this time we use the eject property set to true.

Once they’ve been ejected, the ”participant-left” event is emitted, another event that we’re already listening for:

  const addDailyEvents = (dailyCallFrame) => {
    dailyCallFrame
      .on('participant-left', handleParticipantLeft)
      // … See source code

When this event is received, the participants object is updated in handleParticipantLeft() and the UI updates to reflect the change:

  const handleParticipantLeft = (e) => {
    console.log(e.action);
    setParticipants((p) => {
      const currentParticipants = { ...p };
      delete currentParticipants[e.participant.session_id];
      return currentParticipants;
    });
  };
An admin’s view of being ejected from the call by a call owner.
An admin’s view of being ejected from the call by a call owner

And, like that, we can promote participants to admins or remove them from the call!

Conclusion

In today’s post we looked at sample code that demonstrates how to use the updateParticipant() instance method with the canAdmin property to share admin privileges with other call participants. We also looked at ejecting call participants with the updateParticipant() instance method.

To learn more about admin privileges and some of the topics we couldn’t cover in detail today, check out these related blog posts:

Never miss a story

Get the latest direct to your inbox.