Optimize for Low Bandwidth Networks - iOS
When participants join a call from a weak or unstable network, the available bandwidth may not be enough to support the call at its default media settings. VideoSDK provides several controls that you can use to adapt the experience to changing network conditions.
This guide covers recommended approaches for managing the media each participant sends and receives, including choosing the video quality to publish, controlling incoming video, and keeping participants informed as conditions change. These options can help you reduce bandwidth usage and maintain a reliable call experience.
Before the Call
A pre-call screen is where the participant confirms permissions and picks the camera, microphone, and speaker to use. On a connection you expect to be weak, it is also the point at which you choose the media configuration the participant joins with.
Prepare the Participant
-
Use the pre-call screen to request permissions and confirm device selection before the meeting is initialized, so the participant does not discover a problem after joining. See Precall Setup.
-
Choose the media configuration at this point and pass it to
initMeeting()as a custom track. A participant who joins on a constrained connection with a lighter configuration starts the call in a usable state, rather than starting at a high quality that the connection cannot sustain. -
Where you already know the conditions your participants call from, for example a regional deployment or a known field environment, set those defaults for every participant rather than leaving the configuration at its default.
There is no connection measurement available before the call on iOS. Choose the starting configuration from what you know about your participants, then adapt during the call using the events and controls described in Improve the In-Call Experience.
Configure the Meeting
Use the following settings to control how a participant publishes and receives media. Select the settings based on the participant's network conditions and the requirements of your application.
Configure Published Media
- Pass a configured camera track to
initMeeting()throughcustomCameraVideoStream, and a configured microphone track throughcustomAudioTrack. If a track is not provided, VideoSDK uses its default configuration, which ish720p_w1280pfor the camera andspeech_standardfor the microphone. - Create the camera track with
createCameraVideoTrack()and the microphone track withcreateAudioTrack(). Both accept anencoderConfig, while the camera track also acceptsfacingMode,multiStream,codec,bitrateMode, andmaxLayer. encoderConfigselects the capture resolution. The 16:9 presets areh90p_w160p,h180p_w320p,h360p_w640p,h540p_w960p,h720p_w1280p, andh1080p_w1920p. The 4:3 presets areh120p_w160p,h180p_w240p,h240p_w320p,h360p_w480p,h480p_w640p,h720p_w960p, andh1080p_w1440p.- For constrained connections, consider
BitrateMode.BANDWIDTH_OPTIMIZEDfor the camera to reduce bandwidth usage at the selected resolution.BitrateMode.BALANCEDis the default, andBitrateMode.HIGH_QUALITYraises the bitrate for connections that can support it. See Optimize Video Track for the bitrate used by each configuration. bitrateModeapplies to the camera track. The audio bitrate comes from the audioencoderConfigalone, so choose the audio preset to suit the connection. The two speech presets,speech_low_qualityandspeech_standard, both encode at 24 kbps with discontinuous transmission enabled, which keeps audio light and stops sending during silence. Themusic_standard,standard_stereo,high_quality, andhigh_quality_stereopresets raise the bitrate to between 40 kbps and 192 kbps and turn discontinuous transmission off, so prefer a speech preset on a weak connection unless the call genuinely carries music. See Optimize Audio Track for the available audio configurations.- You can also configure
codecto suit your application's requirements. The default isVP8. Codec support varies by device, so see Video Codecs before changing it, and note the interaction with simulcast described in Keep Simulcast Enabled. - To enforce an upper limit on the amount a participant can send, set Maximum Send Bitrate Per Participant for the API key in the VideoSDK Dashboard. The limit applies to every session using that API key and overrides higher bitrate settings configured in your application. See Optimize Audio and Video Usage for details.
Screen share on iOS is started with enableScreenShare(), which does not take an encoder configuration, so the published screen-share quality is not configured from your application. Where a weak connection has to carry a screen share, reduce the camera configuration instead to leave more of the connection for it. See Screen Share.
Keep Simulcast Enabled
- Enable
multiStreamon bothcreateCameraVideoTrack()andinitMeeting(). The camera then publishes multiple video quality layers, and VideoSDK can deliver an appropriate layer to each participant based on the receiving conditions and requested video quality. - With
multiStreamdisabled, the camera publishes a single video layer at the configured resolution. All participants then receive that same published layer, even when their network conditions or video layouts differ. multiStreamis also what the controls in Optimize Incoming Video act on. Adaptive subscription andsetQuality()both choose between published layers, so they have nothing to select from on a single-layer track.maxLayercaps how many layers a track publishes, acceptsMAX_LAYER_2orMAX_LAYER_3, defaults toMAX_LAYER_3, and applies only whenmultiStreamis enabled. It never adds layers beyond what the capture resolution allows: a third layer requires a longer edge of at least 960 pixels and a second requires at least 480, so on a lowerencoderConfigthe two values can produce the same result.- See Simulcast (Adaptive Bitrate) for what each value publishes.
Simulcast is supported on VP8 only. Requesting AV1, VP9, or H264 together with multiStream falls back to single-layer encoding and reports onError with code 3009 (ERROR_MULTISTREAM_NOT_SUPPORTED). Keep the camera on VP8 when you want every receiver to get a layer suited to its own connection.
Choose the Participant's Media Mode
Use the mode parameter of initMeeting() to control whether a participant publishes and receives media.
| Mode | Description |
|---|---|
SEND_AND_RECV | Publishes and receives media. This is the default. |
RECV_ONLY | Receives media without publishing. Consider this for participants who only need to watch or listen, or when upload bandwidth is limited. |
SIGNALLING_ONLY | Does not publish or receive media. Consider this when the participant does not need real-time media. |
- Choose a mode that matches the participant's role and network conditions.
- Use
changeMode()to switch between modes during the meeting.
CONFERENCE and VIEWER are deprecated and will be removed in a future version. Use SEND_AND_RECV in place of CONFERENCE, and SIGNALLING_ONLY in place of VIEWER.
Keep the Default Transport Protocol
initMeeting()accepts apreferredProtocolofUDP_ONLY,UDP_OVER_TCP, orTCP_ONLY. The default isUDP_OVER_TCP, which prefers UDP and falls back to TCP where UDP is blocked. See Initialise Meeting.- Keep the default on weak networks. It already handles restrictive networks that block UDP, without giving up UDP where it is available.
TCP_ONLYis worth reaching for only where UDP is known to be unavailable. Real-time media on a lossy connection is generally better served by UDP, because retransmissions and in-order delivery add delay to media that is no longer useful by the time it arrives.
Improve the In-Call Experience
Network and media conditions can change during a meeting. Use VideoSDK events and statistics to keep participants informed when the connection is reconnecting, a stream is interrupted, or media conditions affect their experience.
The in-call controls described below require an active meeting. While the connection is RECONNECTING, calls such as setQuality(), pause(), resume(), pauseAllStreams(), enableAdaptiveSubscription(), and changeMode() do nothing and report onError with code 3065 (ERROR_MEETING_RECONNECTING). This is worth planning for on a weak network, because a drop is exactly when your application is most likely to attempt an adjustment. Hold the intended change and apply it once onMeetingStateChanged reports CONNECTED.
Keep Participants Informed
Use call and media events to provide clear feedback when conditions change during the meeting. Depending on your application, this can be a banner, status indicator, or other in-call message.
| Signal | What it tells you | Recommended response |
|---|---|---|
onQualityLimitation | Reports a limitation affecting the local participant's outgoing media. The type can be BANDWIDTH, CONGESTION, or CPU, the state can be DETECTED or RESOLVED, and timestamp carries the time the event occurred. | When a limitation is detected, consider informing the participant. For bandwidth- or congestion-related limitations, you can also reduce the quality of the published video, for example by replacing the camera track with a lighter one as described in Configure Published Media. A connection sitting near its limit can report DETECTED and RESOLVED in turn, so hold a short delay before showing or clearing the message rather than binding it directly to the event. |
onMeetingStateChanged | Indicates a change in the meeting connection state, including RECONNECTING when the connection is interrupted. See Meeting Connection Events. | Show a clear reconnecting message or status indicator so the participant knows the connection is being restored. |
The state event on a stream | Reports the health of a stream as ACTIVE, FREEZE_DETECTED, FREEZE_RESOLVED, STUCK, or ENDED. Register for it with on() on the MediaStream, which returns an identifier you can later pass to off(). | Show feedback on the participant's tile while their stream is frozen or stuck, so the interruption is explained rather than appearing as a broken tile. Clear the feedback when the stream reports FREEZE_RESOLVED or ACTIVE. |
onStreamPaused and onStreamResumed | Report that adaptive subscription paused or resumed a stream from a participant, with the kind of stream and the reason. A stream is paused with reason muted or leastDominance, and resumed with reason networkStable or adaptiveSubscriptionsDisabled. These events are triggered only while adaptive subscription is enabled. See Scalability for Large Participant. | Show feedback on the tile while its video is paused, so the participant understands the tile is intentionally quiet rather than broken. |
getVideoStats() and getAudioStats() | Provide statistics for a participant's media, including values such as bitrate, rtt, jitter, and packetsLost. getShareStats() provides the same for a screen-share stream. See Understanding Call Quality and Monitoring Network Quality. | Use these statistics when your application needs to identify or display more detailed information about the quality of a stream. |
Optimize Incoming Video
Incoming video is usually the largest part of a participant's bandwidth usage, and a participant only benefits from the quality their layout can actually display. We recommend enabling adaptive subscription and matching each participant's requested quality to the size of the tile that renders it.
| Recommendation | How |
|---|---|
| Enable adaptive subscription | Call enableAdaptiveSubscription() after the meeting is joined to let VideoSDK adjust which video streams are subscribed to, based on participant visibility and current network conditions. Use disableAdaptiveSubscription() to turn it off again. See Scalability for Large Participant. |
| Match quality to the tile | Use setQuality() with .low, .medium, or .high to request a layer for a participant, and call it again when your layout changes the size of their tile. A thumbnail in a grid rarely needs more than .low, while a participant filling the screen is worth .high. The default is .high. See Selecting the Video Quality Layer. |
| Pause streams that are not visible | Use pause() on a stream while a participant is outside the visible area and resume() when they return. This matters most in paginated or scrolling layouts, where tiles leave the screen but keep receiving video. See Layout and Grid Management. |
| Pause a whole media kind | Use pauseAllStreams() to stop receiving one kind of media entirely, and resumeAllStreams() to restore it. Passing "video" pauses incoming video, and omitting the parameter pauses audio, video, and screen share together. Consider this for an audio-only fallback when a connection cannot sustain video. |
| Prioritise the participants that matter | Use pin() with .CAM, .SHARE, or .SHARE_AND_CAM to mark the participants your layout gives prominence to, and unpin() when that changes. Every participant receives onPinStateChanged when the pin state is updated. |
Optimize Data Transfers
Call media is not the only traffic using the participant's connection. Image captures and application updates also consume bandwidth, so keeping these transfers efficient can help preserve bandwidth for the call, especially on weak networks.
Capture Images Efficiently
captureImage()captures a frame from the local participant's camera at its capture resolution. It can only be called on the local participant and requires the camera to be enabled, so the image always comes from the camera itself rather than from a received video layer.- For use cases such as document capture or identity verification, this means the image quality does not depend on the published video quality. There is no need to raise the published quality in order to obtain a higher-resolution image.
- Pass
heightandwidthwhen a smaller image is sufficient. If these are not provided, the dimensions of the local camera stream are used. The captured image is delivered to thecompletionclosure.
To obtain an image from another participant, request it over pubSub and have that device capture and transfer it:
| Recommendation | How |
|---|---|
| Request the capture from the camera owner | Publish a request on a pubSub topic, then capture the image locally on the device that receives it. See Image Capturer. |
| Transfer the image through temporary storage | Upload the Base64 image with uploadBase64File(), share the returned URL over pubSub, and retrieve it with fetchBase64File(). This keeps a large payload off the pubSub channel, which other in-call updates share. See File Sharing. |
Optimize Message Delivery
Chat, reactions, application state, and other updates can also use bandwidth during a call. Choose the delivery options based on how important the data is and how quickly it becomes outdated.
| Use case | Recommendation |
|---|---|
| Cursor positions, typing indicators, or other frequently changing updates | Set realtimeOverflow to .drop on PubSubSubscribeOptions so the newest updates are delivered and stale ones are discarded rather than queued. |
| Updates that should survive a brief delay | Leave realtimeOverflow at its default of .queue and set maxQueue, so a subscriber that falls behind can catch up. The oldest messages are dropped once the queue overflows. |
| Persisted messages delivered when a participant joins | Set oldMessageLimit on PubSubSubscribeOptions when the full message history is not required, and newMessageLimit to cap how fast live messages are delivered. |
| Updates intended for specific participants | Set sendOnly in the options passed to publish() so the message is delivered only to the participant IDs that need it, rather than to everyone in the call. |
| Messages that do not need to reach late joiners | Leave persist out of the options passed to publish(), or set it to false, so the message is not stored and replayed to participants who join later. |
Dropped messages are reported through onMessageDrop regardless of which overflow policy is in use, so handle it whenever your application cannot tolerate a gap. See PubSub for the full set of publish and subscribe options.
API Reference
The API references for all the methods and events utilized in this guide are provided below.
- initMeeting()
- createCameraVideoTrack()
- createAudioTrack()
- CustomVideoTrackConfig
- CustomAudioTrackConfig
- BitrateMode
- EncodingLayer
- Mode
- PreferredProtocol
- changeMode()
- enableScreenShare()
- enableAdaptiveSubscription()
- disableAdaptiveSubscription()
- pauseAllStreams()
- resumeAllStreams()
- uploadBase64File()
- fetchBase64File()
- setQuality()
- VideoQuality
- captureImage()
- getVideoStats()
- getAudioStats()
- getShareStats()
- pin()
- unpin()
- pause()
- resume()
- on()
- StreamState
- onQualityLimitation
- onMeetingStateChanged
- onStreamPaused
- onStreamResumed
- publish()
- PubSubSubscribeOptions
- RealtimeOverflow
Got a Question? Ask us on discord

