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:

If a user experiences bandwidth constraints, Adaptive Subscriptions will pause the video streams in the following priority order:
- Muted Participants
- Least Dominant Speakers
- 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
- SDK Version: 4.0.0 or later
- iOS Version: see Supported Device OS
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)")
}
}
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 audioleastDominance: Participant hasn't spoken much in the room
Resume Reasons:
adaptiveSubscriptionsDisabled: Feature was disablednetworkStable: 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: falseand receive only the participants you show - Call
setViewPort(width:height:)with the size of each video tile
Got a Question? Ask us on discord

