Skip to main content
Version: 3.x.x

Simulcast (Adaptive Bitrate) - Android

Simulcast is a technique used in real-time video communication where a single camera publishes several versions of the same video at once. Each version is called a layer, and has its own resolution and bitrate.

VideoSDK then delivers the appropriate layer to each participant, based on their requirements and network conditions.

Why Simulcast Matters​

Without simulcast, one camera publishes a single version of its video, and everyone receives it. That version has to suit every participant at once, so it is set either high enough for the participant with the best connection, which leaves the participant on a weak connection with frozen video, or low enough for the weakest connection, which leaves everyone else with a soft picture they did not need to settle for.

Simulcast removes that compromise. Because several layers are already being published, VideoSDK can give each participant a different one, and can move a participant to a lighter layer the moment their connection tightens and back up when it recovers.

Configuring Simulcast​

Simulcast is configured when creating a video track using VideoSDK.createCameraVideoTrack(). The two parameters that control simulcast are multiStream and maxLayer. If you never pass a custom camera track, the webcam takes multiStream from createRoom() instead.

multiStream​

multiStream determines whether a camera track publishes multiple video layers or a single layer. It is true by default, so simulcast is enabled unless you explicitly disable it.

One participant publishes 720p, 360p, and 180p layers, and VideoSDK sends each viewer the layer that suits them

Setting multiStream to false publishes a single video layer. Every participant then receives the same layer regardless of their screen size or network conditions.

One participant publishes a single 720p stream, and every viewer receives 720p

The number of layers depends on the resolution of the track's encoderConfig.

encoderConfig ResolutionLayers
Less than 480 pixels on the longer edge1
480–959 pixels2
960 pixels or higher3

For example, a 1280x720 track can publish:

320x180 → Low
640x360 → Medium
1280x720 → High

Each layer is scaled down from the video the camera actually captures. If the device cannot capture the requested resolution, the layers are smaller than listed, and fewer of them may be published.

maxLayer​

maxLayer limits the maximum number of layers published by a track. It accepts EncodingLayer.MAX_LAYER_2 or EncodingLayer.MAX_LAYER_3, and null means EncodingLayer.MAX_LAYER_3. It applies only when multiStream is true. To publish a single layer, set multiStream to false.

For example, a 1280x720 track publishes:

maxLayer = MAX_LAYER_3 → 320x180, 640x360, 1280x720
maxLayer = MAX_LAYER_2 → 320x180, 1280x720
multiStream = false → 1280x720

With maxLayer set to EncodingLayer.MAX_LAYER_2, the middle layer is omitted, so the track publishes only the lowest and highest layers, which preserves both a high-quality option and a low-bandwidth fallback while encoding one stream fewer. This is worth reaching for on devices where encoding three streams costs more CPU or battery than the middle layer is worth. With multiStream set to false, the track publishes only its full resolution.

info

maxLayer never adds layers beyond what the encoderConfig allows, and there is no middle layer to drop on a track that publishes two. On CustomCameraTrackConfig.H360P_W640P or CustomCameraTrackConfig.H480P_W640P, MAX_LAYER_2 and MAX_LAYER_3 produce the same result.

Refer to the Optimize Video Track guide for the full set of VideoSDK.createCameraVideoTrack() parameters and for examples of passing a custom track to a room.

Selecting the Video Quality Layer​

Once multiple video layers are published, your application can control which layer each participant receives.

danger

setQuality() chooses between published layers, so it has nothing to act on when a track was created with multiStream set to false. That track publishes one layer, and every participant receives it.

setQuality() sets the layer for a remote participant. It takes a VideoQuality: VideoQuality.LOW, VideoQuality.MEDIUM or VideoQuality.HIGH. It is a suspend function, so call it from a coroutine. Reach for it when your layout already tells you how much detail a view needs, so you can set the level from your own rules. A speaker in the main stage takes VideoQuality.HIGH, and the same participant in a sidebar takes VideoQuality.LOW.

The participant's videoQuality property holds the value you last requested, starting at VideoQuality.HIGH. Calling setQuality() with that same value does nothing, so you can call it on every layout pass.

Refer to the Layout and Grid Management guide for recommended quality levels for different layouts.

Example​

// Give the main stage speaker the sharpest layer, and everyone else a light one
private fun applyGridQuality(activeSpeakerId: String) {
lifecycleScope.launch {
for (participant in room!!.participants.values.toList()) {
try {
participant.setQuality(if (participant.id == activeSpeakerId) VideoQuality.HIGH else VideoQuality.LOW)
} catch (e: VideoSDKException) {
// For example, this participant's webcam is off
Log.w("Quality", "Couldn't set the quality for ${participant.id}: ${e.message}")
}
}
}
}
note

The requested quality is a preference, not a guarantee. If the participant's network cannot support the requested layer, VideoSDK may deliver a lower layer and move back to the requested quality when the connection improves.

note

setQuality() has no effect when called on the local participant.

setScreenShareQuality() does the same for a remote participant's screen share. It takes a VideoQuality: VideoQuality.HIGH, VideoQuality.MEDIUM or VideoQuality.LOW. It has an effect only when the screen share is published with several layers, for example from a screen share track created with multiStream set to true.

Example​

lifecycleScope.launch {
try {
participant.setScreenShareQuality(VideoQuality.HIGH)
} catch (e: VideoSDKException) {
Log.w("Quality", "Couldn't set the screen share quality: ${e.message}")
}
}

Capping the Quality You Send​

You can also limit what your own webcam sends, which saves upload bandwidth.

  • setWebcamQuality() caps the quality of the webcam video you send. VideoQuality.HIGH sends every layer, VideoQuality.MEDIUM stops the largest one, and VideoQuality.LOW sends only the smallest, so other participants receive at most that quality. It takes a VideoQuality, like setQuality(), and is a suspend function. It needs your webcam to be on, and throws a VideoSDKException with code 3062 when the webcam is off, or 3063 when the cap can't be applied. The cap stays in force when you turn the webcam off and on again.
  • dynaCast, a parameter of createRoom(), lets VideoSDK lower the quality of the webcam video you send while no participant is watching its larger layers, and raise it again when someone is. It defaults to false, and never goes above the cap set with setWebcamQuality().

Neither has any effect on a webcam that publishes a single layer, such as one with multiStream set to false, or a codec other than VP8.

Example​

val room = VideoSDK.createRoom(
roomId = roomId,
token = token,
participantName = "John Doe",
dynaCast = true,
)

// Later, while your webcam is on
lifecycleScope.launch {
try {
room.setWebcamQuality(VideoQuality.MEDIUM)
} catch (e: VideoSDKException) {
Log.e("Quality", "Couldn't cap the webcam quality: ${e.message}")
}
}

Checking the Current Video Layer​

Use getVideoStats() to check which video quality layer you are receiving from a remote participant. It returns a JSONArray, or null when no video stream is active.

Each entry includes:

  • currentSpatialLayer — the layer currently being received.
  • preferredSpatialLayer — the layer that was requested.
  • currentTemporalLayer — the temporal layer currently being received.
  • preferredTemporalLayer — the temporal layer that was requested.

Layers are numbered from 0, and a higher number is a higher-quality layer. On a track publishing three layers, 2 is the sharpest and 0 is the lightest. When currentSpatialLayer is below preferredSpatialLayer, a lighter layer than the one requested is being delivered, usually because the connection does not support the requested one yet.

A layer that is not known yet is left out of the entry. For example, preferredSpatialLayer is missing until a layer is requested for that participant, so read these fields with opt().

The stats also include metrics such as bitrate, codec, size, rtt, jitter, and packetsLost, plus limitation for your own video, which can help diagnose the current video quality.

Refer to the Understanding Call Quality guide for more information about these metrics.

Example​

val videoStats = participant.getVideoStats()
if (videoStats != null && videoStats.length() > 0) {
val stats = videoStats.getJSONObject(0)
Log.d("Layer", "Layer in use: " + stats.opt("currentSpatialLayer"))
Log.d("Layer", "Layer requested: " + stats.opt("preferredSpatialLayer"))
}
note

These statistics are a snapshot taken when the method is called.

Simulcast and Codecs​

The codec a track is encoded with determines whether it can publish layers at all.

  • VP8, the default, supports multiStream, so a track using it publishes layers as described above.
  • Refer to the Video Codecs guide for which codecs support multiStream on Android, and for how to handle the cases where a codec is not supported.

API Reference​

The API references for all the methods and events utilized in this guide are provided below.

Got a Question? Ask us on discord