Skip to main content
Version: 4.x.x

Scalability Guide for Large Participant Rooms

Overview​

This guide explains how to optimize performance when hosting video rooms with 10+ participants by using these features:

Adaptive Subscriptions: Prioritizes audio/video streams based on who's speaking

On-Demand Receiving: Receives a participant's mic and webcam only when you ask for them

These features help solve common challenges in large rooms, such as excessive bandwidth consumption and poor quality for users with network constraints.

Common Challenges in Large Rooms​

1. Ineffective Stream Prioritization​

Problem: All streams receive equal treatment regardless of importance.

Impact:

  • Users with limited bandwidth experience poor quality
  • Audio quality suffers when competing with video for resources
  • Important speakers may not be clearly heard or seen

Solution Features​

Adaptive Subscriptions​

This feature optimizes bandwidth by prioritizing active speakers and managing stream quality.

Key Benefits:

  • Identifies and prioritizes dominant speakers
  • Manages bandwidth constraints
  • Ensures clear audio even under network limitations
  • Gives highest priority to pinned participants

How It Works:
The system analyzes speaking patterns to detect active participants and adjusts stream priorities accordingly. For bandwidth-constrained users, it pauses or lowers the quality of non-essential video streams. Pinned participants always receive the highest priority in stream allocation.

Visual Illustration:

Viewport illustration

If a user experiences bandwidth constraints, Adaptive Subscriptions will pause the video streams in the following priority order:

  1. Muted Participants
  2. Least Dominant Speakers
  3. Dominant Speakers

If you want to achieve pin/unpin functionality, you can refer to this documentation

On-Demand Receiving​

By default, your app receives every participant's mic and webcam as soon as they turn them on. With on-demand receiving, it receives a participant's media only when you ask for it, so you can skip the streams you do not show.

Key Benefits:

  • Saves bandwidth on streams that are not on screen
  • Stops receiving a participant's media without muting them for anyone else

Implementation Guide​

Prerequisites​

Enabling Adaptive Subscriptions (BETA)​

You can toggle this feature using these methods. Both are async throws, so call them with try await inside a Task after you join the room:

// Enable adaptive subscriptions
Task {
do {
try await room?.enableAdaptiveSubscription()
} catch {
print("Failed to enable adaptive subscription: \(error)")
}
}

// Disable adaptive subscriptions
Task {
do {
try await room?.disableAdaptiveSubscription()
} catch {
print("Failed to disable adaptive subscription: \(error)")
}
}
note

If the request fails, both methods throw a VideoSDKError. For example, they throw ERROR_ACTION_PERFORMED_BEFORE_ROOM_JOINED (3035) before you join the room, and ERROR_ROOM_RECONNECTING (3065) while the room is reconnecting.

Recommended Implementation:

// Enable on room join

extension RoomViewController: RoomEventListener {

func onRoomJoined() {
Task {
do {
try await room?.enableAdaptiveSubscription()
} catch {
print("Failed to enable adaptive subscription: \(error)")
}
}
}

}

Stream Pause/Resume Events​

When a video stream is paused or resumed, the system triggers events with specific reasons. These events do not include the participant. They go to the listeners you added to that participant with addEventListener(_:), so to tell participants apart, add a separate listener to each one, for example the view that shows that participant.

// Listen for video stream pause events

extension RoomViewController: ParticipantEventListener {

func onStreamPaused(kind: StreamKind?, reason: String) {
print("onStreamPaused reason : \(reason)")
}

func onStreamResumed(kind: StreamKind?, reason: String) {
print("onStreamResumed reason : \(reason)")
}

}

Pause Reasons:

  • muted: Participant has turned off their audio
  • leastDominance: Participant hasn't spoken much in the room

Resume Reasons:

  • adaptiveSubscriptionsDisabled: Feature was disabled
  • networkStable: Network conditions have improved

The reason can also be empty, so handle the reasons you need and ignore the others.

Developer Tip: When a stream is paused (e.g., due to network issues), always keep users in the loop by displaying a helpful UI indicator. For instance, render a message such as: "Paused: Waiting for stable connection..."

Receiving Media On Demand​

Pass autoConsume: false to createRoom(). Your app then receives a participant's mic and webcam only after you call consumeMicStreams() or consumeWebcamStreams() on that participant. Until then, onStreamEnabled() does not fire for that media.

room = try VideoSDK.createRoom(
roomId: roomId,
token: token,
participantName: "John Doe",
autoConsume: false
)

Both methods are async throws. Call them after the room is joined. Participants who were already in the room are reported through onParticipantJoined() before onRoomJoined(), so start receiving them from onRoomJoined(). In this example, hasJoined is a Bool property of your view controller that starts as false.

extension RoomViewController: RoomEventListener {

func onRoomJoined() {
hasJoined = true
// Participants who were already in the room
room?.participants.values.forEach { receiveMedia(of: $0) }
}

func onParticipantJoined(_ participant: Participant) {
// Participants who join later
if hasJoined {
receiveMedia(of: participant)
}
}

func receiveMedia(of participant: Participant) {
Task {
do {
try await participant.consumeMicStreams()
try await participant.consumeWebcamStreams()
} catch {
print("Could not receive media of \(participant.displayName): \(error)")
}
}
}

}

To stop receiving a participant's media, for example when their tile leaves the screen, call stopConsumingMicStreams() or stopConsumingWebcamStreams(). They are also async throws. The participant is not muted for anyone else. Call the consume method again to receive their media again. These methods take effect only when the room was created with autoConsume set to false.

Matching Video Quality to the Tile Size​

Call setViewPort(width:height:) on a remote participant with the size of their video view, in pixels. The server then sends a video quality that fits the view, so a small tile does not use a full-resolution stream. Call it again when the view is resized. It throws a VideoSDKError when this device receives no webcam video from that participant, for example while their webcam is off.

func updateViewPort(of participant: Participant, videoView: UIView) {
let scale = videoView.traitCollection.displayScale
let width = Int(videoView.bounds.width * scale)
let height = Int(videoView.bounds.height * scale)

Task {
do {
try await participant.setViewPort(width: width, height: height)
} catch {
print("setViewPort failed: \(error)")
}
}
}

Saving Upload Bandwidth​

Pass dynaCast: true to createRoom(). The server then lowers the quality of the webcam video you send while no one is watching its larger layers, and raises it again when someone is. It has no effect when multiStream is off.

Best Practices​

  • Enable adaptive subscriptions at the start of every room
  • Design your UI to display indicators when a stream is paused
  • Pin important participants to ensure they receive the highest streaming priority
  • In large rooms, pass autoConsume: false and receive only the participants you show
  • Call setViewPort(width:height:) with the size of each video tile

Got a Question? Ask us on discord