Using Daily’s custom session data to share dad jokes in a video call

Daily offers a new custom session data feature that enables call participants to share client-side state scoped to a Daily video session with other participants. In our last post about Daily’s new custom session data feature, I went over when and how to use this new client-side state functionality. I also covered our other options for client-side data sharing with Daily and explained when to use each one.

Now, let’s dig into some code by building a small demo app that utilizes custom session data within a Daily video call.

What we’re building

We’re going to build a very basic vanilla JavaScript video call application that has an additional feature: sharing dad jokes in a meeting (hopefully, that's not ASCII for too much!)

Two video call participants laughing at a dad joke
Two hilarious video call participants share a laugh

Aside from the regular microphone and camera call controls, this application will feature a “Tell a joke!” button. When any participant clicks the button, a random dad joke will be set in Daily’s session state, synced across all participants—including those that join after the joke is generated.

As hilarious as I am, I did not write the jokes you’ll see in this demo. Instead, I’m using icanhazdadjoke—a free API to retrieve dad jokes on demand. The source of the jokes is really up to you–you can use an API like I did here, hard-code your own jokes, or even hook this up to retrieve jokes from your own dad-a-base.

Once a joke is retrieved, I’ll store it inside the call’s custom session data. This state will be propagated to all existing and future participants via a ”meeting-session-state-updated” event, which the clients will consume to display the new joke.

Running the sample locally

You can see the full code for this sample in our samples repository.

To run this small sample app locally, create a free Daily account and a public Daily room. Then, run the following commands in your terminal:

git clone
cd daily-samples-js/samples/client-sdk/custom-session-data
npm i && npm run dev

With that, let’s go ahead and build this thing!

Setting up the DOM

Because this tutorial is focused specifically on showing you how to use Daily’s new custom session data feature, I won’t dig too much into the call setup logic. Suffice to say you’ll need an home page with a join form that a user can fill out to join a Daily video call.

You can find my setup for this in the sample app’s minimal index.html file:

<!DOCTYPE html>
    <title>Daily Custom Session Data</title>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link href="./style.css" rel="stylesheet" />  
        This application requires JavaScript to run. Please enable JavaScript in
        your browser.
    <div id="header">
      <span>custom session data sample</span><span>powered by <a href="">//daily</a></span>
    <div id="container">
        <div id="controls">
            <form id="join">
              Room URL: <input type="url" id="url" required />
              <button type="submit">Join</button>
            <div id="incall">
              <button id="joke" disabled>Tell a joke!</button>
              <button id="mic" disabled>Toggle Microphone</button>
              <button id="cam" disabled>Toggle Camera</button>
              <button id="leave" disabled>Leave</button>
        <div id="jokeEle" class="hidden"></div>
        <div id="participants"></div>
    <script crossorigin src=""></script>
    <script  type="module" src="./index.js"></script>

As you can see, the index file has a "container" div which includes:

  • All call controls (containing the join form, cam/mic toggles, and the all-important “Tell a joke!” Button)
  • The joke element (containing the current dad joke, if any)
  • A participants div (containing all call participant video tiles)

The join form takes a single input value: the URL of the Daily room the user wants to join. This URL can be found on the room details page in your Daily developer dashboard.

Finally, I import the daily-js front-end library in a script tag.

Now, let’s hook up this form to some actual JavaScript.

Setting up the Daily video call

The JavaScript entry point to the application is index.js, which lives right alongside index.html.

This is where I’ll initialize instrumenting the call itself, using a handler for the ”DOMContentLoaded" event:

window.addEventListener('DOMContentLoaded', () => {
  const callObject = setupCallObject();

The first thing I do above is set up and return an instance of the Daily call object. Then, I pass said call object over to the join form setup function.

Let’s take a look at the call object setup.

Setting up the call object and relevant event handlers

In setupCallObject(), I instantiate and configure the call object as follows:

function setupCallObject() {
  const callObject = window.DailyIframe.createCallObject();
  const participantParentEle = document.getElementById('participants');

  // Set up relevant event handlers
    .on('meeting-session-state-updated', (e) => {
    .on('joined-meeting', (e) => {
    .on('left-meeting', () => {
    .on('participant-updated', (e) => {
    .on('participant-joined', (e) => {
    .on('participant-left', (e) => {
    .on('track-started', (e) => {
    .on('error', (e) => {

    return callObject;

Let’s go through what’s happening above:

The first step to setting up a Daily call object is instantiating it. I do so with the createCallObject() daily-js factory method.
Next, I retrieve the "participants" DOM element, which will eventually hold each call participant’s video tiles.
After that, I set up some event handlers to deal with various Daily video call events during the call lifecycle (more on these below.)
Finally, I return the call object back to the caller.

In the interest of focusing on our new custom session data feature, I will not go through each of the configured event handlers in detail. However, you can find an overview of each handler and a link to the relevant code below:

  • “meeting-session-state-updated” - This event I will go through in more detail below, as it’s the star of the show when it comes to our custom session data handling!
  • “joined-meeting” - Emitted when the local participant joins a Daily room. In this handler, I set up the local participant’s video tile, enable in-call controls, and enable the joke button.
  • ”left-meeting -  Emitted when the local participant leaves their current Daily call. This is where I disable the call controls, remove participant video tiles, and disable the “Tell a joke!” button.
  • ”participant-updated” - Emitted when the local or remote participant object is updated. I use this to detect when the local participant has toggled their camera or microphone, and update their call controls accordingly.
  • ”participant-joined" - Emitted when a remote participant joins the Daily video call. I use this handler to add a new participant element to the DOM when someone new joins.
  • ”participant-left" - Emitted when a remote participant leaves the Daily video call. In this handler, I remove any DOM elements associated with a departed participant.
  • ”track-started” - Emitted when a new track has started on a local or remote video call participant. I use this event to keep each participant’s video and audio tracks up to date.
  • ”error” - Emitted when Daily encounters an error during any part of the call. Here, I simply log the error as unrecoverable and take the participant back to the join form. You’ll want some more nuanced error handling here if this were a production application.

Now that we’ve got a call object ready to go, we can use it to hook up the actual join form.

Setting up the call join form

I’ve tucked away most of the call control logic (joining, leaving, track handling) into the call/controls.js file, which is where setupJoinForm() resides as well. All joke-related functionality using our custom session data feature remains in index.js, since that’s what we’re going to be looking at in more detail.

I won’t dig too far into any of the non-joke-related controls because, again, we’re keeping laser focus on the custom session data feature in this post. Suffice to say setupJoinForm() is an exported function which gets the join form from the DOM and, on submission, invokes the call object join() method to join a Daily video call:

export function setupJoinForm(callObject) {
  const joinForm = getJoinForm();
  joinForm.onsubmit = (ev) => {

    // Disable join button to avoid double-join attempts
    const btn = joinForm.getElementsByTagName('button')[0];
    btn.disabled = true;

    const roomURLInput = joinForm.getElementsByTagName('input')[0];
    try {
      callObject.join({ url: roomURLInput.value });
    } catch (e) {
      console.error('Failed to join Daily room', e);

I suggest checking out  call/controls.js if you’re curious to see how other call controls are handled, including camera and microphone toggling. And if you’re curious about how call participants’ video tiles themselves are handled, take a look at call/dom.js.

Let the jokes begin

Okay, so we’ve got our call lifecycle all hooked up. Let’s get to the fun part.

Setting up the “Tell a joke!” button

The joke button is enabled and a handler is set up for it when the local participant joins the Daily video call. This logic is triggered by Daily’s ”joined-meeting” event:

function getJokeButton() {
  return document.getElementById('joke');

function setupJokeButton(callObject) {
  const btn = getJokeButton();
  btn.onclick = () => {
    getJoke().then((joke) => {
      // Update the custom session data with the new joke!

Above, I first retrieve a joke button from the DOM via getJokeButton(). This function simply retrieves the DOM element with the "joke" ID.

Then, I set up an onclick event handler for the retrieved element. When the button is clicked, I call the getJoke() function to retrieve a joke (I’ll go through this shortly). Then, I invoke the setMeetingSessionData() call object instance method to populate the custom session data with the new joke.

By setting the meeting data, I’m storing the joke in Daily's call session state. Once the data is stored, all call participants can consume it either immediately or on demand, depending on the use case (I’ll go through the methods of doing this below).

Finally, once the onclick handler is defined, I call enableJokeButton() to make the button active:

function enableJokeButton() {
  const btn = getJokeButton();
  btn.disabled = false;

Anyone in the call can click this button in their local app instance and generate a new joke. You could optionally show the button only to select participants, but for this sample app I decided that joke-telling should be a universal right.

"Tell a joke!" button
Joke-telling button

Retrieving a joke

You saw the mention of getJoke() up above. This is where I use the icanhazdadjoke API to retrieve a dad joke:

async function getJoke() {
  const url = '';
  try {
    const res = await fetch(url, {
      headers: {
        Accept: 'application/json',
    const json = await res.json();
    const { joke } = json;
    return joke;
  } catch (err) {
    console.error('failed to fetch dad joke: ', err);
    return null;

Above, I make a GET request to and return the joke string from the resulting JSON. The joke button’s onclick event handler (which we went through up above) can then pass this joke string on to Daily with the aforementioned setMeetingSessionData() call object instance method.

The setMeetingSessionData() instance method also allows you to specify what kind of merge behavior you prefer. By default, a call to set new meeting session data replaces all other data, which is what we want for this demo. But you could also instruct Daily to perform a shallow merge by specifying ”shallow-merge" after your data argument.

So, we have a joke. But is a joke even a joke if there’s no one around to laugh at it?

Let’s display our new joke to all call participants.

Displaying the dad joke

So, some hilarious call participant decided to generate a new meeting-wide joke to share with everyone in the call (including themselves!) How do call participants actually get to see the new joke?

This is where the ”meeting-session-state-updated” event comes in. This event is emitted on the Daily call object instance when the meeting session state, including any custom data anyone has set, has been updated.

You can also retrieve any custom session data (or other meeting session state) on demand with the meetingSessionState() call object instance method.

I mentioned setting up a handler for this event in our call object setup above. Here it is:

    .on('meeting-session-state-updated', (e) => {
      // When a meeting session state update event is received,
      // if it contains a joke, set the joke in the DOM.
      const { joke } =;
      if (!joke) return;

The meeting session state object is part of the event payload for ”meeting-session-state-updated". Above, I retrieve the custom data property within it. If it has no joke property, I exit out. This means someone set some funky state that we will ignore!

But if the custom session data does contain a joke, I call setJoke()  to display it in the DOM:

function setJoke(joke) {
  const ele = getJokeEle();
  ele.innerText = joke;

Above, I retrieve the joke element (that being the div with the ”jokeEle" ID):

function getJokeEle() {
  return document.getElementById('jokeEle');

I then set its inner text to the given joke string and remove the "hidden" attribute, if it has one (now that we have a joke, we should show the element).

And that’s it! Now, all existing participants and any new participants that join the call will get to witness the joke. When a new participant joins, Daily will emit the ”meeting-session-state-updated” to them automatically.

A new call participant joins and sees an existing joke
New participant joining the demo app

Leaving the call

When the local participant leaves the Daily call, we need to take them back to the join form and hide any in-call elements, such as the video tiles and the shared joke. This is all done within the ”left-meeting” event handler:

    callObject.on('left-meeting', () => {
      // When the local participant leaves the call,
      // disable their call controls and remove
      // all media elements from the DOM.

Above, I disable all call controls (including the joke button), remove all participant elements,  and hide the joke from the user’s UI. They are now ready to join another call and be regaled with a new dad joke.


In this tutorial, I’ve walked you through a sample app utilizing Daily’s new custom session data features. You learned how to set custom session data with setMeetingSessionData() and respond to data changes with the ”meeting-session-state-updated” event. You also learned that you can retrieve all meeting session state on demand with the meetingSessionState() call object instance method.

Do you have any fun use cases for custom session data, or any thoughts about the functionality? Continue the conversation over at peerConnection, our WebRTC community.

Never miss a story

Get the latest direct to your inbox.