Implementing a pre-call attendant list with Daily’s video API
Tags: tutorial, webrtc, programming, api
Published: 2023-09-05 23:18:17

_By [Liza Shulyayeva](https://www.daily.co/blog/author/liza/)_

Imagine: You have a scheduled video meeting with a group of people that you’re not too familiar with. You join, and there is _one other person there_. Someone you don’t know well (or at all). You make awkward small talk while waiting for others to join. Or worse, they are joining on a 4G connection from a cave and you spend a few minutes trying to communicate through “Can you hear me now?”, “Maybe try turning your video off.”, or “How far are you from the cave entrance?”

This could all be avoided if you wait until more people are in the call before joining so that you can blend into the crowd.

To facilitate this act of social self-preservation, the video call app you’re joining needs to show how many participants are _already_ in the call before you join.

That’s what we’ll be building today with [Daily Prebuilt](https://www.daily.co/products/prebuilt-video-call-app/), Daily’s full-featured video call embed.

## What we’re building
The app contains two main client-side components:

1. An entry page, where a user can create and enter a Daily video call room:

![Daily Prejoin Presence demo entry page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g2m1vavn70nsjivffj0d.png)
_Entry page_

2. A call page, which contains a video call frame on the left and a sidebar on the right:

![Participant in a Daily pre-join lobby, with existing participants shown on the right](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rxnmtew9f2ns04ak6q21.gif)
_Pre-join lobby showing existing participants on the right_

The sidebar contains a link to invite others to join the call. Underneath that, there is a “participants” list. This list is populated when the participant enters the call’s pre-join lobby, and can be updated by refreshing the page.

Now that we know what we’re building, let’s go through the core technical components.

## Technical components
On the client side, I’m using [Daily’s Client SDK for JavaScript](https://docs.daily.co/reference/daily-js) by importing our `daily-js` NPM package via a script tag. The client portion is written in vanilla JavaScript.

For creating a Daily room and retrieving the list of attendants, I’m using two static Netlify functions written in Go. However, you’ll find this post useful even if you’re implementing this feature in JavaScript or another language, since the relevant Daily REST API endpoints remain the same regardless of language. They are:

- [`/rooms`](https://docs.daily.co/reference/rest-api/rooms/create-room) to create a room. I won’t focus too much on this part, because the focus in this post will be presence retrieval.
- [`/rooms/:room/presence`](https://docs.daily.co/reference/rest-api/rooms/get-room-presence) to retrieve presence information about the room.

## Running the demo locally
You can follow along with the tutorial [on GitHub](https://github.com/daily-demos/daily-prejoin-presence), but if you want to run the demo locally, use the following steps:

1. Sign up for a free [Daily account](https://daily.co/signup) if you don’t already have one.
2. Copy your [Daily API key](https://dashboard.daily.co/developers) from the developer dashboard.
3. [Install Go](https://go.dev/) if you don’t already have it installed.
4. Clone the [demo repository](https://github.com/daily-demos/daily-prejoin-presence).
5. Copy the `sample.env` file from the repo root into a new `.env` file and paste the API key you copied as the value for `DAILY_API_KEY`. _Do not submit this file to version control!_
6. Run `npm i && npm run dev`

Your default browser should open the web app automatically. If it doesn’t, navigate there manually using the localhost port shown in your terminal, which is usually `http://localhost:8888/`.

Now that you have the app up and running, let’s go through the implementation.

## Defining the DOM
The entry point for the demo is `/src/index.html`, with the most interesting part being the call containers and `daily-js` import:

```
<div id="container">
  <div id="entry">
    <button id="createRoom">Create Room</button>
  </div>
  <div id="call" class="hidden">
    <div id="callFrame"></div>
    <div id="sidebar">
      <div id="invite">Invite URL:
        <br /><a id="inviteURL" href=""></a></div>
      <hr />
      <div id="presence" class="hidden">
        The following participants are already in the call:
        <ul id="participants"></ul>
      </div>
    </div>

  </div>
</div>
<script crossorigin src="https://unpkg.com/@daily-co/daily-js"></script>
<script src="./call.js" type="module"></script>
```

Above, I’m creating an `"entry"` div that has a single room creation button.

I’m then defining a `"call"` div, which is where the Daily Prebuilt call frame will live once a call is joined. This also contains the sidebar element with the invite URL and presence list.

Finally, I’m adding two script tags: one to import daily-js and another to import the local JavaScript entry point: `/src/call.ts`.

Let’s take a closer look at that call file next.

## Instrumenting the DOM

When the DOM is loaded, set up the two main lobby flows:

- Call creation: The participant is creating a new room and joining it.
- Call join (via query parameters): The participant is joining an existing Daily room via a link shared from another participant already in the call.

The logic looks as follows:

```
window.addEventListener('DOMContentLoaded', () => {
  setupCreateButton(createRoom);

  // If room URL and name params were provided, join that room
  const params = new URLSearchParams(window.location.search);
  const roomURL = params.get('roomURL');
  const roomName = params.get('roomName');
  if (roomURL && roomName) {
    joinRoom(roomURL, roomName);
    return;
  }

  // If room URL and name params were not provided, show
  // entry element (with call creation button)
  showEntry();
});
```

Let’s go through an overview of both flows now.

## Creating a Daily video call room

A Daily room is created when the application user clicks the “Create Room” button on the demo home page:

![Room creation button](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8ojbri5ij7f3gwuhkjb6.png)
_Room creation button_

As I said, I won’t focus too much on this part of the process here. Let’s go through just a high-level overview.

- When the index page loads, I set up an `onclick` handler for the room creation button above via `setupCreateButton()`.
- When this button is clicked, the createRoom() function in `call.js` will be invoked:

```
async function createRoom() {
  const url = `/.netlify/functions/createRoom`;
  const errMsg = 'failed to create room';

  // Create Daily room
  let res;
  try {
    res = await fetch(url);
  } catch (e) {
    console.error('failed to make Daily room creation request', e);
  }

  // Retrieve body, if it exists in the response
  let body;
  try {
    body = await res.json();
  } catch (e) {
    console.warn('No body present in room creation response');
  }

  // Check status code
  if (res.status !== 200) {
    const msg = `${errMsg}. Status code: ${res.status}`;
    console.error(`failed to create Daily room: ${msg}`, body);
    return;
  }
  joinRoom(body.url, body.name);
}
```

This will, make a POST request to my `createRoom` Netlify function endpoint, the definition of which can be found in [`/.netlify/functions/createRoom/main.go`]((https://github.com/daily-demos/daily-prejoin-presence/blob/cd231ccf2ab73c2ae6e190707ab850cd86e99284/netlify/functions/createRoom/main.go)).

I won’t go through the whole server-side creation flow here, but a thing worth noting is that I generate each room with a very important property: [`enable_prejoin_ui: true`](https://docs.daily.co/reference/rest-api/rooms/create-room#enable_prejoin_ui).

This property will ensure a participant won’t land straight in the meeting when they call Daily’s [`join()`]((https://docs.daily.co/reference/daily-js/instance-methods/join)) call frame instance method. They’ll instead see a pre-join UI where they can select their devices, test their camera, and—critically—_see who’s already in the call:_

![Screenshot of existing participants list](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5vr2eryr96d6l391sv59.png)
_Participants list_

With that said, let’s go ahead and cover the implementation of the join flow and presence data retrieval.

## Joining the Daily video call

As we already saw in the overview above, there are two paths to joining the Daily call in this demo:

- The call creator joins the call after creating a room.
- A call participant joins the call directly when certain query parameters are passed in the URL (namely `roomURL` and `roomName`).

Both of these scenarios go through the same [`joinRoom()` function](https://github.com/daily-demos/daily-prejoin-presence/blob/cd231ccf2ab73c2ae6e190707ab850cd86e99284/src/call.js#L65):

```
function joinRoom(roomURL, roomName) {
  showCall();

  // Set up the call frame and join the call
  const callContainer = document.getElementById('callFrame');
  const callFrame = window.DailyIframe.createFrame(callContainer, {
    showLeaveButton: true,
  });
  callFrame.on('left-meeting', () => {
    showEntry();
    clearPresence();
    callFrame.destroy();
  });

  callFrame.join({ url: roomURL });

  updateDisplayedURLs(roomURL, roomName);

  // Fetch existing participants and show them next to the call frame
  fetchParticipants(roomName);
}
```

Alright, let’s go through what’s happening above:

1. First, I show the call div I created in `index.html` by calling `showCall()` in `dom.js`. This function also makes sure the original entry element (with the call creation button) is hidden, since we don’t need it when the user enters a video call.
2. Next, I instantiate the Daily Prebuilt call frame within my call container div. I do this by calling the `daily-js [createFrame()](https://docs.daily.co/reference/daily-js/factory-methods/create-frame)` factory method and passing the call container object to it.
3. I set up an _event handler_ for Daily’s `[’joined-meeting’](https://docs.daily.co/reference/daily-js/events/meeting-events#joined-meeting)` event. This event will fire when the local participant joins the call (_not_ just the prejoin lobby, but the video call itself). When this happens, I’ll hide the presence div - the participant will be able to see who’s in the call through Prebuilt’s own UI one they’re in.
4. Next, I call the `join()` call frame instance method. This will join the given Daily room URL. However, note that if we have Prebuilt’s pre-call lobby feature enabled (which in this case, we do), this call with result in them joining the _lobby_.
5. Then, I update the DOM with an invite URL to this call.
6. Finally, I call `fetchParticipants()`, passing the room name as a parameter. _This is where the magic happens._

Let’s go through the participant fetching next.

## Fetching video call presence from Daily

On the client, [fetching a list of participants](https://github.com/daily-demos/daily-prejoin-presence/blob/cd231ccf2ab73c2ae6e190707ab850cd86e99284/src/call.js#L117) in the Daily room looks as follows:

```
async function fetchParticipants(roomName) {
  const url = `/.netlify/functions/presence?`;
  const errMsg = 'failed to fetch room presence';

  let res;
  try {
    // Add room name to the presence endpoint query parameters
    const reqURL =
      url +
      new URLSearchParams({
        roomName,
      });

    // Call out to the Netlify presence endpoint
    res = await fetch(reqURL);
  } catch (e) {
    console.error('failed to make Daily room presence request', e);
  }

  // Retrieve body from response, if it exists
  let body;
  try {
    body = await res.json();
  } catch (e) {
    console.warn('No body present in room presence response');
  }

  // Check status code
  if (res.status !== 200) {
    const msg = `${errMsg}. Status code: ${res.status}`;
    console.error(`failed to fetch presence for Daily room: ${msg}`, body);
    return;
  }

  // Count the number of participants in the returned body
  const count = body.length;
  if (count === 0) {
    // If no one is there yet, update the UI to state that
    addToPresenceList('Nobody here yet!');
  } else {
    // If there is someone in the room, add their
    // username or ID to the presence list in the DOM
    for (let i = 0; i < body.length; i += 1) {
      const p = body[i];
      const label = p.userName ? p.userName : p.id;
      addToPresenceList(label);
    }
  }
  // Show the presence DOM element
  showPresence();
}
```
The in-line comments above guide you through exactly what the function is doing, but in short: I call out to my presence Netlify function endpoint to fetch a list of participants already in the room. I then update the UI accordingly and show the presence DOM element to the user.

But how is that list of participants actually fetched? Let’s hop over to the server to find out.

## Fetching presence information from Daily’s REST API (with Go!)

The Netlify function for fetching Daily presence can be found in the [`netlify/functions/presence/main.go`](https://github.com/daily-demos/daily-prejoin-presence/blob/cd231ccf2ab73c2ae6e190707ab850cd86e99284/netlify/functions/presence/main.go) file within the demo repo. I suggest checking that out to see the full context. Let’s cover the most important part below: the logic to actually call out to Daily’s REST API for presence information.

Before I show you the code, let’s cover roughly what we’re going to do:

1. Make a call to Daily’s [`rooms/:name/presence`](https://docs.daily.co/reference/rest-api/rooms/get-room-presence) REST API endpoint for presence data
2. Parse the response data into a [byte slice](https://go.dev/blog/slices-intro).
3. Return an error (with the body, if relevant) if the response code is not `200`.
4. Otherwise, unmarshal the body as JSON into a pre-defined struct to hold all the presence data.

Let’s start with the primary building blocks - understanding the format in which Daily’s REST API will return presence data. For this, we can refer to [the documentation for this endpoint](https://docs.daily.co/reference/rest-api/rooms/get-room-presence#example-request) as an example:

```
{
   "total_count":1,
   "data":[
      {
         "room":"w2pp2cf4kltgFACPKXmX",
         "id":"d61cd7b2-a273-42b4-89bd-be763fd562c1",
         "userId":"pbZ+ismP7dk=",
         "userName":"Moishe",
         "joinTime":"2023-01-01T20:53:19.000Z",
         "duration":2312
      }
   ]
}
```

With that in mind, I can define a Go struct that will follow the above data format:

```
// presenceRes is the expected response format
// from Daily's REST API when retrieving room presence.
type presenceRes struct {
	Data []Participant `json:"data"`
}
```

Well, that’s not much… As you can see, above we have a struct with a single field defined: `Data`. The field is a slice of `Participant`, which is mapped to the `"data"` JSON property.

In Go, constructs starting with a lowercase letter are considered “unexported” (accessible only by members within the same package/directory). Anything starting with an uppercase letter is “exported” (accessible by all members within the project).

`presenceRes` above is defined as an _unexported_ struct, but its Data field is _exported_. Why? [`json.Unmarshal()`](https://pkg.go.dev/encoding/json#Unmarshal) only unmarshals _exported_ fields. So even though `presenceRes` should not be accessible to any package outside of `presence`, its `Data` property needs to be exported for unmarshalling purposes.

Now, let’s take a look at the `Participant` struct definition:

```
// Participant represents a Daily participant.
// who is already in a Daily room
type Participant struct {
	ID   string `json:"id"`
	Name string `json:"userName"`
}
```

As you can see above, each instance of `Participant` will contain the participant’s ID and name, as returned from Daily. These are the only pieces of data I’ll use for this demo.

Now that we have all the building blocks in place, let’s see what [`getPresence()`](https://github.com/daily-demos/daily-prejoin-presence/blob/cd231ccf2ab73c2ae6e190707ab850cd86e99284/netlify/functions/presence/main.go#L72) looks like:

```
// getPresence retrieves all participants already in the given Daily room
func getPresence(roomName, apiKey, apiURL string) ([]Participant, error) {
	endpoint := fmt.Sprintf("%s/rooms/%s/presence", apiURL, roomName)
	// Make the actual HTTP request
	req, err := http.NewRequest("GET", endpoint, nil)
	if err != nil {
		return nil, fmt.Errorf("failed to create GET request to room endpoint: %w", err)
	}

	util.SetAPIKeyAuthHeaders(req, apiKey)

	// Do the thing!!!
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("failed to get room: %w", err)
	}

	// Parse the response
	resBody, err := io.ReadAll(res.Body)
	if err != nil {
		return nil, fmt.Errorf("failed to read presence response body: %w", err)
	}

	if res.StatusCode != http.StatusOK {
		return nil, util.NewErrFailedDailyAPICall(fmt.Errorf("%d: %s", res.StatusCode, string(resBody)))
	}

	var pr presenceRes
	if err := json.Unmarshal(resBody, &pr); err != nil {
		return nil, fmt.Errorf("failed to unmarshal Daily response to participant slice: %w", err)
	}
	return pr.Data, nil
}
```

The above function completes all the steps I outlined in the beginning of this section and returns the `Data` property of the `presenceRes` instance I get by unmarshalling the response body back to the caller, which in this case is my endpoint [handler](https://github.com/daily-demos/daily-prejoin-presence/blob/cd231ccf2ab73c2ae6e190707ab850cd86e99284/netlify/functions/presence/main.go#L32).

And that’s it! We’ve now covered both the client and server-side portions of implementing a pre-join presence list for the betterment of video-calling humankind.

## What’s next?
There are many ways you can expand and tailor this functionality to your own use case. Just a few ideas to play around with include:

- Making the list refresh dynamically by refetching the participant list, instead of requiring a page refresh.
- Showing an avatar next to the participant’s name. Check out our [user data documentation](https://docs.daily.co/reference/daily-js/instance-methods/set-user-data#main), which could come in handy here.
- Display more details about each participant, such as how long they’ve been waiting for, to application administrators.Check out our [guide on using Daily’s meeting tokens](https://docs.daily.co/guides/privacy-and-security/meeting-tokens) as a starting point for this feature.

## Conclusion

In this post, we went through a basic implementation of pre-join video call presence display using Daily’s WebRTC API platform.

Armed with this knowledge, you have the power to help introverted call users everywhere to avoid the awkwardness of being the second person joining a group call.

Don’t hesitate to [reach out to our support team](https://www.daily.co/company/contact/support/) if you’d like any help implementing pre-join presence in your own video app. Or head over to [peerConnection, our WebRTC community](https://community.daily.co/) to keep the conversation going.