Skip to main content

TL;DR

What rtc-js users most often confuse are actually two different things:
  • Capture resolution: decides how large the raw video captured by the camera is
  • Publishing simulcast: decides whether the same video is encoded into multiple layers and sent together
From first principles, the “low stream” isn’t another camera, nor a separate mode; it’s essentially just a low-resolution encoding layer of the same video track. So:
  • To capture sharper video, change the capture parameters
  • To control “send one layer or two”, change simulcasts in the publish parameters
  • To “send only the low stream”, you don’t “enable low-stream mode”; you send a single low-resolution stream

SDK default behavior

The Web SDK’s built-in camera presets behave as follows by default:
  • CameraPresets['720p']: publishes camera_big + camera_small by default
  • CameraPresets['1080p']: publishes camera_big + camera_small by default
  • CameraPresets['360p'] / CameraPresets['180p']: no default simulcast configuration
The current built-in low-stream parameters are: This means that if you write it like this:
you’re already sending, by default:
  • one camera_big
  • one camera_small

The concepts, separated

1. Capture resolution

Capture resolution is determined by the preset passed to createLocalCameraTrack(...), or by the parameters passed to startCapture(...):
It affects “how sharp the source is”.

2. Publishing simulcast

Publishing simulcast is determined by VideoPublishOptions.simulcasts when calling publishLocalTrack(...):
It affects “how many encoding layers are sent into the channel”.
Key point: Changing only the capture resolution doesn’t turn off the low stream automatically; changing only simulcasts doesn’t change the camera’s actual capture resolution either.

Scenario 1: change the high-stream resolution but keep simulcast

This is the most common need. To do it:
  • First set the camera capture resolution to the target value
  • Then keep simulcasts
For example, change the high stream to 1080p while still sending a 320 × 180 low stream:
If you want to customize the low-stream parameters, you can override them explicitly:

Scenario 2: send only the high stream, no low stream

Don’t just change width / height here; explicitly empty simulcasts.
When to use it:
  • The channel is small and doesn’t need layer switching
  • Your app only consumes one main video
  • You want to reduce uplink encoding and bandwidth cost
Note: CameraPresets['720p'] / ['1080p'] include the low stream by default. If your goal is “send only the high stream”, you must explicitly pass simulcasts: [] to override the default.

Scenario 3: send only a single low-resolution stream

When people say “send only the low stream”, from an encoding standpoint what they usually want is:
  • Don’t send camera_big
  • Send only a single low-resolution stream
In that case, don’t think in terms of “720p high stream + low stream”; instead create a low-resolution camera track directly and publish only that layer:
What this approach really is:
  • There’s only one video track
  • That track’s spec is low resolution
  • Whether it’s called camera_small is just naming for your app’s semantics; underneath, it isn’t “the secondary layer of the high stream turned on by itself”

Scenario 4: customize simulcast parameters

If the default 320 × 180 / 250 Kbps doesn’t fit your app, customize it directly:
Recommendations:
  • Decide the main layer’s resolution based on what your UI actually needs; don’t chase 1080p blindly
  • The secondary layer mainly serves thumbnails, small windows, and grids, so its resolution and bitrate should be clearly lower than the main layer’s
  • If the channel often has poor networks or multiple videos on screen at once, keeping the low stream is usually more stable than sending only the high stream

Don’t create two camera tracks manually to simulate simulcast

For example, this approach isn’t recommended:
The reasons are straightforward:
  • It becomes two independent app-level tracks rather than multiple encoding layers of the same video
  • Remote users have to handle two streams separately, which muddles the semantics
  • Local capture, encoding, and bandwidth costs are also higher
The right approach:
  • When you need simulcast, use one camera track + simulcasts
  • When you only need a single low-resolution stream, send just one low-resolution stream directly

How remote users see simulcast

In the TrackInfo seen by remote users, two fields matter:
  • fallback_ids: which lower layers the main layer can fall back to
  • variant: whether this is a simulcast secondary layer
Your app should generally follow this rule:
  • Subscribe to the main layer track first
  • Don’t display a secondary layer with variant === true as a separate new video
The SDK follows the same principle when auto-subscribing: it skips secondary layers and leaves the actual layer switching to the SFU and fallback_ids.

Recommendations