Call and Participant State

When you join a call, we’ll automatically expose state in 3 different places: the Stream Video Client, the Call, and the participants.

var clientState = streamVideo.state;
var callState = call.state;
var participants = call.state.value.callParticipants;

Call State

When a Call is created, users can subscribe to receive notifications about any changes that may occur during the call’s lifecycle. To access the state of a call, use call.state.value to obtain the latest CallState snapshot, or use valueStream to listen for real-time changes to the CallState.

This functionality is particularly useful for determining which parts of the UI or application to render based on the current state or lifecycle of the ongoing call.

For example, you may want to display an indicator to users when a call is being recorded:

StreamBuilder<CallState>(
  stream: call.state.valueStream, // Subscribe to state changes
  builder: (context, snapshot) {
    final state = snapshot.data;
    if (state.isRecording) {
      return CallRecordingUI();
    } else {
      return RegularCallUI();
    }
  },
),

Optimizing State Updates with Partial State

The call state contains a lot of different fields, and therefore updates often. In the example above we are only interested in isRecording, but the builder will still be called every time anything in the state changes. To improve this the Call object also contains a property for partialState. The partialState requires a selector callback which you can use to filter the data needed.

For example, you can get a Stream that indicates if the call is being recorded or not:

Stream<bool> isRecordingStream = call.partialState((state) => state.isRecording);

You can also use records to return multiple values, for example for a livestream:

Stream<({bool isBackstage, DateTime? endedAt})> partialStateStream =
    call.partialState((state) => (isBackstage: state.isBackstage, endedAt: state.endedAt));

If you use a custom class to filter the data, make sure that class implements equality correctly, for example by using equatable. If you create a new object without equality checks the Stream might be updated on every state change.

Using PartialCallStateBuilder Widget

To use the partialState in your widgets you can use the regular StreamBuilder or the PartialCallStateBuilder to make your life a bit easier.

PartialCallStateBuilder(
  call: call,
  selector: (state) => state.isRecording,
  builder: (context, isRecording) =>
      isRecording ? CallRecordingUI() : RegularCallUI(),
);

Call State Properties

The following fields are available on the call state:

AttributeDescription
callCidThe type and id of the call.
currentUserIdThe user ID of the local user.
createdByUserThe user that created the call.
createdByUserIdThe id of a user that created the call.
isRingingFlowIf this call has ringing set to true.
sessionIdThe current session ID for the call.
statusThe current call state - see next section for more information.
settingsThe settings for this call.
preferencesThe call preferences - see below for more information.
egressContains URL for playlist of recording.
rtmpIngressContains the RTMP ingest URL used for live streaming.
isRecordingIf the call is being recorded or not.
isBroadcastingIf a call is broadcasting (to HLS) or not.
isTranscribingIf transcriptions are active or not for this call.
isCaptioningIf closed captions are active or not for this call.
isBackstageIf a call is in backstage mode or not.
isAudioProcessingIf audio processing (e.g., noise cancellation) is active.
videoInputDeviceVideo input device currently set for the call.
audioInputDeviceAudio input device currently set for the call.
audioOutputDeviceAudio output device currently set for the call.
ownCapabilitiesWhich actions current user have permission to do.
callParticipantsThe list of call participants.
callMembersThe list of call members (including those not currently in the call).
createdAtWhen the call was created.
startsAtWhen the call is scheduled to start.
endedAtWhen the call ended.
updatedAtWhen the call was updated.
startedAtWhen the call session was started.
liveStartedAtWhen call was set as live.
liveEndedAtWhen call was set as not live.
timerEndsAtTimestamp when the call will end, if maxDuration was set for the call.
blockedUserIdsIds of blocked users for this call.
participantCountCurrent participants count on this call.
anonymousParticipantCountCurrent anonymous participants count on this call.
iOSMultitaskingCameraAccessEnabledWhether multitasking camera access is enabled on iOS.
capabilitiesByRoleWhat different roles (user, admin, moderator etc.) are allowed to do.
customCustom data provided for this call.

Call Preferences

Call preferences configure various aspects of call behavior and performance including timeouts, reaction behavior, statistics reporting, and video publishing options. For comprehensive information about configuring and using call preferences, see the Call Preferences Configuration guide.

Computed Properties

Some properties are computed from the state for convenience:

// Get the local participant
var localParticipant = call.state.value.localParticipant;

// Get all participants except yourself
var otherParticipants = call.state.value.otherParticipants;

// Get participants who are currently speaking
var activeSpeakers = call.state.value.activeSpeakers;

// Get members who are being called but haven't answered
var ringingMembers = call.state.value.ringingMembers;

// Check if you created the call
var createdByMe = call.state.value.createdByMe;

Understanding Call Status

The status property of the CallState object indicates the current state of the call. Depending on where you are in the call lifecycle, CallStatus can have one of the following possible values.

Call StatusDescription
CallStatusIdleIndicates that there is no active call at the moment.
CallStatusIncomingIndicates that there’s an incoming call, and you need to display an incoming call screen.
CallStatusOutgoingIndicates that the user is making an outgoing call, and you need to display an outgoing call screen.
CallStatusConnectingIndicates that the SDK is attempting to connect to the call.
CallStatusReconnectingIndicates that the SDK is attempting to reconnect to the call. The number of attempts can be set via the attempt property.
CallStatusReconnectionFailedIndicates that the SDK failed to reconnect.
CallStatusMigratingIndicates that the SDK is attempting to migrate from one SFU to another.
CallStatusConnectedIndicates that the user is connected to the call and is ready to send and receive tracks.
CallStatusDisconnectedIndicates that the call has ended, failed, or has been canceled. The exact reason can be accessed via the DisconnectedReason property.
CallStatusJoiningIndicates that the user is in the process of joining the call.
CallStatusJoinedIndicates that the user has successfully joined the call.

By checking the CallStatus value in the CallState object, you can determine the current state of the call and adjust your UI accordingly.

// Example of using call status
PartialCallStateBuilder(
  call: call,
  selector: (state) => state.status,
  builder: (context, status) {
    if (status.isConnecting) {
      return ConnectingUI();
    } else if (status.isConnected) {
      return ConnectedCallUI();
    } else if (status.isReconnecting) {
      return ReconnectingUI(attempt: status.attempt);
    }
    return DefaultCallUI();
  },
);

Understanding CallStatusDisconnected Reasons

When a call becomes disconnected, the CallStatusDisconnected status contains a DisconnectReason that explains why the disconnection occurred. Understanding these reasons allows you to handle different scenarios appropriately in your application.

Available Disconnect Reasons

Disconnect ReasonDescription
DisconnectReasonTimeoutThe call timed out.
DisconnectReasonFailureA general failure occurred during the call.
DisconnectReasonSfuErrorAn SFU server error occurred.
DisconnectReasonRejectedCan happen when the ringing call is triggered and the last participant rejects the call.
DisconnectReasonBlockedThe user was blocked from the call.
DisconnectReasonCancelledThe call was cancelled by the caller.
DisconnectReasonEndedThe call was ended.
DisconnectReasonReplacedThe call was replaced by another call.
DisconnectReasonLastParticipantLeftThe last participant left the call and the dropIfAloneInRingingFlow was set to true.
DisconnectReasonReconnectionFailedFailed to reconnect to the call after network issues.

Listening to Call Disconnection

You can listen to call state changes and handle disconnection using partial state:

// Listen to call status changes and handle disconnection
PartialCallStateBuilder(
  call: call,
  selector: (state) => state.status,
  builder: (context, status) {
    if (status.isDisconnected) {
      final disconnectedStatus = status as CallStatusDisconnected;
      final reason = disconnectedStatus.reason;
      
      return DisconnectedCallUI(reason: reason);
    }
    
    // Handle other statuses...
    return RegularCallUI();
  },
);

Using onCallDisconnected Callback

Both StreamCallContainer and LivestreamPlayer provide an onCallDisconnected callback that you can use to handle disconnection events:

// In StreamCallContainer
StreamCallContainer(
  call: call,
  onCallDisconnected: (CallDisconnectedProperties properties) {
    final reason = properties.reason;
    final call = properties.call;
    
    // Handle the disconnection
    _handleCallDisconnected(reason);
    
    // Navigate away or show appropriate UI
    Navigator.of(context).pushReplacementNamed('/home');
  },
);

// In LivestreamPlayer
LivestreamPlayer(
  call: call,
  onCallDisconnected: (CallDisconnectedProperties properties) {
    final reason = properties.reason;
    
    // Handle livestream disconnection
    if (reason is DisconnectReasonEnded) {
      _showStreamEndedDialog();
    } else {
      // Handle other disconnection reasons
    }
  },
);

Handling Participant Limit Reached

When a call has a maximum participant limit configured, you can specifically handle the participant limit reached scenario:

// Complete example with pattern matching
void _handleCallDisconnected(DisconnectReason reason) {
  switch (reason) {
    case DisconnectReasonSfuError(error: final sfuError):
      if (sfuError.code == SfuErrorCode.callParticipantLimitReached) {
        // Handle participant limit reached
        _showDialog(
          title: 'Call Full',
          message: 'This call has reached its participant limit. Please try again later.',
          actions: [
            TextButton(
              onPressed: () => Navigator.of(context).pop(),
              child: Text('OK'),
            ),
          ],
        );
      } 
      break;
    // Handle other disconnect reasons...
  }
}

Participant State

var participants = call.state.value.callParticipants;
var localParticipant = call.state.value.localParticipant;

In the call state, you can find the parameter callParticipants. This parameter allows you to access and manipulate the participants present on the call. By using callParticipants, you can easily map over the participants and observe changes in their configuration. For instance, you can keep track of which participant is currently speaking, which participant is the dominant speaker, and which participant is pinned to the call. Additionally, callParticipants allows you to monitor other changes to the call’s configuration as well.

Overall, callParticipants is a powerful tool that provides you with a lot of control and insight into the call’s current state and configuration. By leveraging this parameter effectively, you can create more advanced and robust call applications.

for (final participant in call.state.value.callParticipants) {
  if (participant.isDominantSpeaker) {
    setState(() => dominantSpeaker = participant);
  }
}

Participant State Properties

The following fields are available on each participant:

AttributeDescription
userIdThe unique user ID of the participant.
rolesThe user’s roles in the call.
nameThe participant’s display name.
imageThe participant’s profile image URL.
customAny custom data added to the user.
sessionIdThe session ID of the participant.
trackIdPrefixReturns the user’s track ID prefix.
publishedTracksReturns the participant’s published tracks (video, audio, screen).
isSpeakingReturns whether the participant is speaking.
isDominantSpeakerReturns whether the participant is a dominant speaker.
isPinnedReturns whether the participant is pinned.
pinThe pin information if the participant is pinned (is it a local or global pin).
isLocalReturns whether the participant is the local user.
isOnlineReturns whether the participant is online.
connectionQualityThe participant’s connection quality.
audioLevelThe current audio level for the user.
audioLevelsList of the last 10 audio levels.
reactionThe current reaction added by the user.
viewportVisibilityThe user’s visibility on the screen.

Track Information

Participants have track information for different media types:

// Access specific tracks
var videoTrack = participant.videoTrack;
var audioTrack = participant.audioTrack;
var screenShareTrack = participant.screenShareTrack;

// Check if tracks are enabled
var isVideoEnabled = participant.isVideoEnabled;
var isAudioEnabled = participant.isAudioEnabled;
var isScreenShareEnabled = participant.isScreenShareEnabled;

Listening to Participant Changes

You can listen to changes in participants using partial state:

// Listen to changes in speaking participants
PartialCallStateBuilder(
  call: call,
  selector: (state) => state.activeSpeakers,
  builder: (context, activeSpeakers) {
    return Column(
      children: activeSpeakers.map((participant) => 
        Text('${participant.name} is speaking')
      ).toList(),
    );
  },
);

// Listen to dominant speaker changes
PartialCallStateBuilder(
  call: call,
  selector: (state) => state.callParticipants
      .where((p) => p.isDominantSpeaker)
      .firstOrNull,
  builder: (context, dominantSpeaker) {
    return dominantSpeaker != null
        ? Text('Dominant speaker: ${dominantSpeaker.name}')
        : const SizedBox.shrink();
  },
);

Combining CallState and CallParticipantState makes building custom UIs and integrations a breeze. If there is a property or API that is not exposed for your specific use case, feel free to reach out to us. We are constantly iterating and exposing APIs based on your feedback.

Client State

// Client state is available in the client object
var clientState = StreamVideo.instance.state;
AttributeDescription
userThe user you’re currently authenticated as.
connectionThe connection state of Stream Video.
activeCallsThe calls you’ve currently joined (when multiple calls are enabled).
activeCallThe call you’ve currently joined (when single call mode is enabled).
incomingCallContains the incoming call if ringing is set to true.
outgoingCallContains the outgoing call if ringing is set to true.
© Getstream.io, Inc. All Rights Reserved.