Incoming Video State

Our servers can detect when a subscriber is on a low-bandwidth connection and may automatically disable selected incoming video streams. This helps prioritize audio quality and reduce total bandwidth usage, improving overall call stability.

When this feature is active, incoming video streams may be paused selectively, resulting in an audio-only experience for some participants. The UI can reflect this state with an icon or message overlay.

Low Bandwidth Optimization

The low bandwidth optimization is enabled by default at SDK level. The SDK integrator may opt out by updating the client capabilities of the Call before joining:

final call = client.makeCall(callType: StreamCallType.defaultType(), id: 'my-call-id');
call.disableClientCapabilities([SfuClientCapability.subscriberVideoPause]);

When enabled it signals to the backend that the client supports dynamic video pausing, allowing the system to optimize media delivery under limited network conditions.

Observing Paused Tracks

When the feature is active, the backend may mark some remote video tracks as paused. These will appear in the participant's pausedTracks property:

call.state.listen((state) {
  final pausedVideoParticipants = state.callParticipants.where(
    (p) => p.isTrackPaused(SfuTrackType.video),
  );

  print('Participants with paused video tracks: $pausedVideoParticipants');
});

This can be used to update the UI, e.g. by displaying an indicator that a participant's video is disabled due to low bandwidth.

Built-in Support

The SDK's default widgets handle paused incoming video tracks out of the box — no additional configuration is needed when using StreamCallContent. Specifically:

  • StreamVideoRenderer detects when a participant's video track is paused and automatically replaces the video feed with a placeholder (typically the user's avatar).
  • StreamParticipantLabel displays a network indicator icon (Icons.network_check) next to the participant's name when their video is paused.

You can customize the indicator color via the pausedVideoIndicatorColor parameter on StreamCallParticipant, or provide a fully custom callParticipantBuilder for complete control.

Custom Handling Example

While the default widgets cover the common case, you may want to react to paused tracks in your own way — for example, showing a banner when one or more participants have their video paused due to low bandwidth.

Using PartialCallStateBuilder, you can efficiently select only the piece of state you care about and rebuild when it changes:

StreamCallContent(
  call: call,
  callAppBarWidgetBuilder: (context, call) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        CallAppBar(call: call),
        PartialCallStateBuilder(
          call: call,
          selector: (state) => state.callParticipants
              .where((p) => p.isTrackPaused(SfuTrackType.video))
              .map((p) => p.name)
              .toList(),
          builder: (context, pausedNames) {
            if (pausedNames.isEmpty) return const SizedBox.shrink();

            return Container(
              width: double.infinity,
              color: Colors.orange.shade800,
              padding: const EdgeInsets.symmetric(
                horizontal: 12,
                vertical: 8,
              ),
              child: Text(
                'Low bandwidth — video paused for: '
                '${pausedNames.join(', ')}',
                style: const TextStyle(
                  color: Colors.white,
                  fontSize: 13,
                ),
              ),
            );
          },
        ),
      ],
    );
  },
)

The selector extracts the list of participant names with paused video from the call state. PartialCallStateBuilder only triggers a rebuild when that derived value actually changes, keeping the rest of the UI untouched. The banner appears automatically when the server pauses tracks and disappears once conditions improve.