Tracking connection quality with Daily's new connectivity test methods

We’ve recently released three new call object instance methods to test connection and network quality:

Each one can also be aborted mid-test with the associated method:

In this post, I’ll go through a small code sample showing you how to use each of these methods.

But first, let’s go through some common use cases for these connection test features.

Video call connection test use cases

We envision three common use cases for our new test methods:

  1. Facilitating local troubleshooting
  2. Tracking quality metrics internally
  3. Informing ideal send setting configuration in the call

Let’s cover each of these in a little more detail.

Facilitating local troubleshooting

With the new connection test methods, developers can implement a pre-join UI in which the user’s connection quality and connectivity can be gauged. If shortcomings are detected, the user can be prompted with relevant suggestions to improve their experience.

For example, if testNetworkConnectivity() indicates a problem connecting to Daily’s TURN servers, the user can be prompted to check their firewall setup (especially if they’re on a corporate network).

Likewise, if users are experiencing degraded connection quality in the call itself, showing an indicator of connection quality with testConnectionQuality() can help participants narrow down which of them is experiencing connection difficulties at the time.

Tracking quality metrics internally

Helping users troubleshoot their local setups is great, but tracking performance metrics or flagging spikes in problematic sessions can help detect potential issues in advance. You can forward output from the test methods above to your existing metrics and telemetry pipeline and respond to any suspicious spikes in network problems accordingly.

Informing ideal send setting configuration

Daily provides reasonable media input and output configuration by default, but some highly customized implementations may benefit from more granular settings. Daily enables developers to fine-tune their send settings by defining their own simulcast layers or picking from a range of convenient presets.

By polling connection quality at regular intervals during the call, your application can respond to network fluctuations by updating the simulcast preset being used via our updateSendSettings() call instance method.

💡
You can also use our network quality events to inform send settings.

Now that we have some primary use cases established, let’s take a look at some code showing the usage of these methods.

What we’re building

This code sample shows a pre-call flow in which a local participant can test their connection quality, TURN server connectivity, and WebSockets connectivity. You, the local participant, will see their own camera feed when they start the test, but will not have joined a Daily room yet. When you click 'Run Tests', the three connectivity test methods mentioned above will be invoked and the results will be shown in the app UI.

Network and connectivity tests running with the user's local video track being shown

Running the sample locally

You can find the sample in our JavaScript samples repository. You do not need a Daily account or a Daily room to run this locally. Perform the following steps:

  1. Clone the repository: git clone https://github.com/daily-demos/daily-samples-js.git
  2. Navigate to the relevant sample directory: cd daily-samples-js/samples/client-sdk/connectivity-tests
  3. Check out the relevant tag: git checkout v1.0
  4. Run npm i && npm run dev
  5. Navigate to the port shown in your terminal in your preferred web browser. This will likely be localhost:8080

Once you have the demo app open in your browser, click the “Run Tests” button:

Run Tests button

You should now see the in-progress test labels and your local camera feed appear. (Be sure to grant camera permissions in your browser if prompted.)

Now that you’ve got the code running locally, let’s walk through how I implemented usage of the new connectivity test methods. I’ve isolated all the logic relevant to this in the index.js file, so we’re going to focus on that.

Setting up the test button and Daily call object

When the DOM loads, the first thing I do is set up the “Run Tests” button:

window.addEventListener('DOMContentLoaded', () => {
  // Set up the primary test button and provide the handler to run when it is clicked.
  setupTestBtn(() => {
    const callObject = setupCallObject();
    setupLeaveBtn(() => {
      leave(callObject);
    });
    startTests(callObject);
  });
});

When the test button is clicked, three things happen:

  1. A new Daily call object is instantiated (setupCallObject()).
  2. The Leave button is set up (setupLeaveBtn()).
  3. The tests are run (runTests()).

Let’s take a closer look at the most important part: call object setup. I’ve left some guiding comments in-line, but I’ll also provide an overview of what’s happening right underneath the code:

function setupCallObject() {
  // If call instance already exists, return it.
  let callObject = window.DailyIframe.getCallInstance();
  if (callObject) return callObject;

  // If call instance does not yet exist, create it.
  callObject = window.DailyIframe.createCallObject();
  const participantParentEle = document.getElementById('participants');

  // Set up relevant event handlers
  callObject
    .on('track-started', (e) => {
      enableControls();
      showTestResults();
      const p = e.participant;
      if (!p.local) return;

      const newTrack = e.track;

      addParticipantEle(p, participantParentEle);
      updateMediaTrack(p.session_id, newTrack);
      if (e.type === 'video') {
        runAllTests(callObject, newTrack);
      }
    })
    .on('error', (e) => {
      // If an unrecoverable error is received,
      // allow user to try to re-join the call.
      console.error('An unrecoverable error occurred: ', e);
      enableTestBtn();
    });

  return callObject;
}

Below are the highlights:

  1. I check if a call instance (which can be a call object or a Daily Prebuilt call frame) already exists. If so, I return it as there’s no need to create a new one.
  2. If a call instance doesn’t already exist, I create one using Daily’s createCallObject() factory method.
  3. I then set up handlers for the two Daily events I’ll be handling: ”track-started" and ”error".
  4. Finally, I return the new call object instance to the caller.

The "track-started” event is where all the magic happens. Here, I add a DOM element for the participant whose track has just started, and update it with the available tracks. For now, this will only be the local participant since we’re not actually joining any Daily video call room here.

Then, I check if the started track is a video track. Daily’s connection quality test and network connectivity test both take a video track as a parameter. So as soon as we get a video track, we’re ready to run the tests.

However, the local participant’s video won’t just start without us explicitly asking them to start their camera. Let’s take a look at where that’s done once the call object instance is created and set up.

Starting the camera to run video call connection tests

Now that the call object instance is all set up with all the handlers we’ll need, it’s time to actually start the tests. This is done I the ”DOMContentLoaded" event handler we covered above, by invoking the demo’s startTests() function and passing the new call object instance to it:

function startTests(callObject) {
  disableTestBtn();

  // Reset results, in case this is a re-run.
  resetTestResults();

  // If local participant already exists and has a track,
  // just run the tests.
  const localParticipant = callObject.participants().local;
  const videoTrack = localParticipant?.tracks?.video?.persistentTrack;
  if (videoTrack) {
    runAllTests(callObject, videoTrack);
    return;
  }

  // If there is not yet a local participant or a video track,
  // start the camera.
  try {
    callObject.startCamera();
  } catch (e) {
    console.error('Failed to start camera', e);
    enableTestBtn();
  }
}

Here’s what’s happening above:

  1. The “Run Tests” button is disabled
  2. The results displayed in the DOM are reset
  3. I check if a local participant already exists and has a video track. If so, I go ahead and call runAllTests() with the participant’s existing video track
  4. If a local participant does not yet exist, or doesn’t have a video track, I call Daily’s startCamera() call object instance method. This will prompt the user for permissions to start their webcam. This, in turn, will trigger that ”track-started" event I covered above.

With all the setup done and handlers hooked up, we’re ready to look at the runAllTests() method:

Running the video call connection tests

I run all three of Daily’s connection tests in parallel as follows:

function runAllTests(callObject, videoTrack) {
  Promise.all([
    testConnectionQuality(callObject, videoTrack),
    testNetworkConnectivity(callObject, videoTrack),
    testWebSocketConnectivity(callObject),
  ]).then(() => {
    enableTestBtn();
  });
}

Once all the tests are done, I re-enable the “Run Tests” button to let the local user run the tests again. Let’s take a look at each of the tests above.

testConnectionQuality(): Test quality of the WebRTC connection

This Daily connection quality test method returns a string reflecting the participant’s current connection quality. The method takes a video track and a duration value (how long you want the test to run for, in seconds). For the purposes of this example, I’ve set the duration to 5 seconds:

function testConnectionQuality(callObject, videoTrack) {
  return callObject
    .testConnectionQuality({
      videoTrack,
      duration: 5, // In seconds
    })
    .then((res) => {
      const testResult = res.result;
      let resultMsg = '';
      switch (testResult) {
        case 'aborted':
          resultMsg = 'Test aborted before any data was gathered.';
          break;
        case 'failed':
          resultMsg = 'Unable to run test.';
          break;
        case 'bad':
          resultMsg =
            'Your internet connection is bad. Try a different network.';
          break;
        case 'warning':
          resultMsg = 'Video and audio might be choppy.';
          break;
        case 'good':
          resultMsg = 'Your internet connection is good.';
          break;
        default:
          resultMsg = `Unexpected connection test result: ${testResult}`;
      }
      updateConnectionTestResult(resultMsg);
    })
    .catch((e) => {
      console.error('Failed to test connection quality:', e);
    });
}

Above, I invoke Daily’s testConnectionQuality() call object instance method. Once a result is returned, I interpret it via a switch statement and update the DOM with a relevant result message. The result object actually contains more data, like packet loss and max round trip time, which I’m not using here in order to keep the sample code simple. Refer to our testConnectionQuality() documentation to learn more about the data being returned.

testNetworkConnectivity(): Check connectivity to Daily’s TURN server

This test method returns a string with an ”aborted”, "passed", or ”failed" value. Like the connection quality test above, I interpret the result in a switch statement and update the DOM with a user-friendly message. Since the logic is very similar to the test we looked at above I won’t paste the code here, but you can check it out on GitHub.

testWebsocketConnectivity(): Check WebSocket traffic support

This Daily instance method returns an object with a result property of four potential string values: ”passed”, ”failed”, ”warning”, or ”aborted”. The object also contains an array of passed, failed, or aborted regions.

You can check out my implementation of this test on GitHub, since its handling is once again very similar to the two tests above.

Now that we’ve looked at running the tests and consuming the results, let’s take a look at the final piece: ending tests early.

Aborting Daily’s connection tests

If a user presses “Leave” while tests are running, I want to abort the remaining tests. I do that in my handler for the “Leave” button:

function leave(callObject) {
  callObject.setLocalVideo(false);

  // Abort any tests that may currently be running.
  callObject.abortTestNetworkConnectivity();
  callObject.abortTestWebsocketConnectivity();
  callObject.stopTestConnectionQuality();

  // Only enable test button again once call object is destroyed
  callObject.destroy().then(() => {
    enableTestBtn();
  });
}

Above, I turn off the user’s local video. I then stop all three tests (if a test isn’t currently running, this will be a no-op) and destroy the call object. Once the call object is destroyed, I re-enable the “Run Tests” button.

With that, our pre-call connection test implementation is complete.

Conclusion

Do you have any questions about using Daily’s new connection and connectivity tests? Reach out to our support team or head over to our Discord community.

Never miss a story

Get the latest direct to your inbox.