Skip to content

Disabling Video for Low-Bandwidth Users in a Broadcast Scenario

Jacob Steele Jun 15, 2023 6:47:37 PM

In a WebRTC conference, ensuring a satisfactory user experience often involves adapting bandwidth to accommodate participants with low connectivity. Most SDKs achieve this by degrading stream quality for everyone if a participant with low bandwidth joins the conference. Server-side Simulcast also addresses this scenario by providing a lower quality stream to the low bandwidth participant without affecting others. However, what happens in cases where Simulcast is not an option? In such cases, you need to disable video streams for low-bandwidth peers and switch them to audio-only mode.

 

Let's explore a real-life use case:

  • You are about to stream a broadcast to 3000 participants in a one-to-many setup.
  • You either do not use an SDK with Simulcast or have opted out due to cost considerations.
  • You plan to send a single high-quality video feed that all users should see as is.

 

This scenario arises frequently, and people often ask how to achieve it. Normally, without any modifications, bandwidth adaptation would adjust the main broadcast feed to a quality that the lowest throughput peer can handle. However, this approach does not make sense for the other 2999 participants - why should their stream quality be degraded as well?

 

One way to address this is to disable the video streams for low-bandwidth peers while keeping their audio connected. Here is an example implementation in LiveSwitch:

/// <reference path="vendor/fm.liveswitch.js" />


// file: main.js

(async function ()
{
  let applicationId = "";
  let sharedSecret = "";
  let gatewayURL = "https://cloud.liveswitch.io/"
  let channel = "broadcast";

  fm.liveswitch.Log.DefaultLogLevel = fm.liveswitch.LogLevel.Debug;
  fm.liveswitch.Log.registerProvider(new fm.liveswitch.ConsoleLogProvider());

  let client = new fm.liveswitch.Client(gatewayURL, applicationId);
  let claim = new fm.liveswitch.ChannelClaim(channel);
  claim.setBroadcast(true); // Useless flag, but I like setting it.
  claim.setDisableSfu(false); // SFU is how we will send this broadcast.
  claim.setDisableSendMessage(false); // Enable this if you use WSS for chat.
  claim.setCanKick(false); // Disable ability to click.
  claim.setCanUpdate(false); // Disable ability to moderate.
  claim.setDisableMcu(true); // Disable MCU (using SFU)
  claim.setDisablePeer(true); // Disable Peer (using SFU)
  claim.setDisableSendAudio(true); // Disable their ability to send audio (viewer).
  claim.setDisableSendData(true); // Enable this if you use datachannels for chat.
  claim.setDisableSendVideo(true); // Disable their ability to send video (viewer).
  claim.setDisableRemoteClientEvents(true); // We are using broadcast media ids, don't need these events (extra bandwidth)
  claim.setDisableRemoteUpstreamConnectionEvents(true); // We are using broadcast media ids, don't need these events (extra bandwidth)
  let token = fm.liveswitch.Token.generateClientRegisterToken(client, [claim], sharedSecret);
  let layoutManager = new fm.liveswitch.DomLayoutManager(document.querySelector("div.player"));
  let statsDiv = document.querySelector("div.stats");
  let reButton = document.querySelector("button.revideo");
  try {
    let channels = await client.register(token);
    let channel = channels[0];
   
    let
openDownstream = () =>
    {
      let remoteMedia = new fm.liveswitch.RemoteMedia();
      remoteMedia.setAudioMuted(false);
      remoteMedia.setVideoMuted(false);
      layoutManager.addRemoteView(remoteMedia.getId(), remoteMedia.getView());

      let audioStream = new fm.liveswitch.AudioStream(remoteMedia);
      let videoStream = new fm.liveswitch.VideoStream(remoteMedia);

      let connection = channel.createSfuDownstreamConnection( "broadcast", audioStream, videoStream );

      connection.addOnStateChange(async (connection) =>
      {
        if (connection.getState() == fm.liveswitch.ConnectionState.Closing || connection.getState() == fm.liveswitch.ConnectionState.Failing) {
          layoutManager.removeRemoteView(remoteMedia.getId());
          remoteMedia.destroy();
          openDownstream();
        }
      });

      let timeout = null;
      connection.addOnNetworkQuality(async (quality) => {
        statsDiv.innerHTML = `${quality * 100}%`;
        if (quality < 0.9) {
          let config = connection.getConfig();
          if (!config.getRemoteVideoDisabled()) {
            config.setRemoteVideoDisabled(true);
            await connection.update(config);
            alert("Disabled video due to bandwidth issues");
          }
          if (timeout) {
            // If they continue to have bandwidth issues, no point in letting them re-try.
            clearTimeout(timeout);
          }
          timeout = window.setTimeout(() => {
            reButton.classList.remove("hide");
          }, 15000);
        }
      });

      reButton.addEventListener("click", async () => {
        reButton.classList.add("hide");
        let config = connection.getConfig();
        config.setRemoteVideoDisabled(false);
        await connection.update(config);
      });
     
      connection.open();
    };
    openDownstream();

  } catch(ex) {
    console.error(ex);
    throw ex;
  }
}());
<!doctype html>

<html class="no-js" lang="">
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta charset="utf-8">
  <meta name="keywords" content="Global Broadcast Event">
  <meta name="description" content="">
  <title>Bandwidth Test</title>
  <style>
    html, body {
      width: 100%;
      height: 100%;
    }

    .hide {
      visibility: hidden !important;
    }
   
    .player
{
      background-color: black;
      width: 100%;
      height: 100%;
    }

    .stats {
      position: fixed;
      top: 20px;
      color: red;
      right: 20px;
    }

    .revideo {
      position: fixed;
      top: 20px;
      right: 80px;
      cursor: pointer;
      z-index: 999;
    }
  </style>
</head>
<body>
  <div class="stats"></div>
  <button class="revideo hide">Re-Enable Video</button>
  <div class="player"></div>
  <script src="js/vendor/fm.liveswitch.js"></script>
  <script src="js/main.js"></script>
</body>
</html>

 

You can also monitor the network quality using the following code snippet:

connection.addOnNetworkQuality((quality) => { }); //Ranges from 0.0 to 1.0 (1.0 being the highest)

 

The quality metric is determined based on received packets versus expected packets and the jitter buffer. When this value drops below 90%, bandwidth adaptation is about to take effect. At that point, we quickly disable incoming video:

config.setRemoteVideoDisabled(true);

await connection.update(config);

 

Since some people may experience minor network fluctuations, it would be unfair to permanently disable video for them. In the provided example, we offer users the opportunity to re-enable video after a brief timeout and upon having a stable network connection:

timeout = window.setTimeout(() => { 
  reButton.classList.remove("hide");
}, 15000);
reButton.addEventListener("click", async () => {
  reButton.classList.add("hide");
  let config = connection.getConfig();
  config.setRemoteVideoDisabled(false);
  await connection.update(config);
});

 

Although this use case is specific, it arises regularly. By following this guide and implementing the provided code in this article, you can ensure a high-quality video feed for the majority of participants while disabling video for low-bandwidth peers. This solution helps optimize the conference experience for all attendees, even in bandwidth-constrained situations.

 

Need assistance in architecting the perfect WebRTC application? Let our team help out! Get in touch with us today!