Calling State and Lifecycle

The call object instance manages everything related to a particular call instance, such as:

  • creating and joining a call
  • performing actions (mute, unmute, send reaction, etc…)
  • manage event subscriptions (call.on('call.session_started', callback), etc…)
  • and many more

Every call instance should be created through the client.call(type, id) helper.

Our StreamVideoClient is responsible for maintaining a WebSocket connection to our servers and also takes care about the API calls that are proxied from the call instance.

As we learned in Joining and Creating Calls guide, a call instance is managed like this:

import { Call, StreamVideoClient } from "@stream-io/video-client";

let client: StreamVideoClient; // ...

const call: Call = client.call(type, id);

// load existing call information from our servers
await call.get();

// Creates the call on our servers in case it doesn't exist. Otherwise,
// loads the call information from our servers.
await call.getOrCreate();

// join the call
await call.join();

// leave the call and dispose all allocated resources
await call.leave();

Every call instance has a local state, exposed to integrators through:

  • call.state.callingState - a getter that returns the current value
  • call.state.callingState$ - an observable that an integrator can subscribe to and be notified everytime the value changes

The call instance is a stateful resource that, once acquired with client.call(), must be disposed of with call.leave(). Failure to dispose of the call properly can result in memory leaks and unexpected behavior.

Calling State

Every call instance has its own local state managed by the SDK.

These values are exposed through the CallingState enum:

import { CallingState, useCallStateHooks } from "@stream-io/video-client";

const { useCallCallingState } = useCallStateHooks();
const callingState = useCallCallingState();

switch (callingState) {
  case CallingState.JOINED:
    // ...
    break;
  default:
    const exhaustiveCheck: never = callingState;
    throw new Error(`Unknown calling state: ${exhaustiveCheck}`);
}

As CallingState is an enum that can be extended at any time by us, it would be good to make sure you use it exhaustively. This way, if you use TypeScript, you can get a compile time error and be notified that there are few more states that you should handle.

Calling States

StateDescription
CallingState.UNKNOWNThe state is unknown. This value is set when Calling State isn’t initialized properly.
CallingState.IDLEA call instance is created on the client side but a WebRTC session isn’t established yet.
CallingState.RINGINGThis is an incoming (ring) call. You are the callee.
CallingState.JOININGThe call join flow is executing (typically right after call.join()). Our systems are preparing to accept the new call participant.
CallingState.JOINEDThe join flow has finished successfully and the current participant is part of the call. The participant can receive and publish audio and video.
CallingState.LEFTThe call has been left (call.leave()) and all allocated resources are released. Please create a new call instance if you want to re-join.
CallingState.RECONNECTINGA network connection has been lost (due to various factors) and the call instance attempts to re-establish a connection and resume the call.
CallingState.RECONNECTING_FAILEDThe SDK failed to recover the connection after a couple of consecutive attempts. You need to inform the user that he needs to go online and manually attempt to rejoin the call.
CallingState.MIGRATINGThe SFU node that is hosting the current participant is shutting down or tries to rebalance the load. This call instance is being migrated to another SFU node.
CallingState.OFFLINENo network connection can be detected. Once the connection restores, the SDK will automatically attempt to recover the connection (signalled with RECONNECTING state).

Calling State transitions: regular path

The following diagram visualizes the expected state transitions when joining a call normally, either as a caller or callee.

stateDiagram-v2
  direction LR
  [*] --> IDLE
  IDLE --> RINGING: call.join({ ring })
  RINGING --> JOINING
  IDLE --> JOINING: call.join()
  JOINING --> JOINED
  JOINED --> LEFT: call.leave()
  LEFT --> [*]

Calling State transitions: reconnects and migration

The following diagram visualizes the expected state transitions when joining a call after a network glitch or when the SFU node hosting the current participant is shutting down or rebalancing the load.

stateDiagram-v2
  direction LR
  [*] --> JOINING
  JOINING --> JOINED: successful join

  JOINED --> RECONNECTING: network glitch/switch
  RECONNECTING --> JOINING: attempt to reconnect
  RECONNECTING --> RECONNECTING_FAILED

  JOINED --> LEFT: call.leave()
  LEFT --> [*]

  JOINED --> MIGRATING: initiate server migration
  MIGRATING --> JOINING: attempt to migrate

  JOINED --> OFFLINE: went offline
  OFFLINE --> RECONNECTING

Example handling

To understand these values better, here is a hypothetical example of how these values can be mapped:

import { CallingState, useCallStateHooks } from "@stream-io/video-client";

const { useCallCallingState } = useCallStateHooks();
const callingState = useCallCallingState();

switch (callingState) {
  case CallingState.UNKNOWN:
  case CallingState.IDLE:
    return renderLobbyScreen();

  case CallingState.RINGING:
    return renderIncomingCallScreen();

  case CallingState.JOINING:
    return renderLoadingScreen();

  case CallingState.JOINED:
    return renderActiveCallScreen();

  case CallingState.LEFT:
    return renderHaveANiceDayScreen();

  case CallingState.RECONNECTING:
  case CallingState.MIGRATING:
    return renderRestoringConnectionScreen();

  case CallingState.RECONNECTING_FAILED:
    return renderGeneralConnectionProblemScreen();

  case CallingState.OFFLINE:
    return renderNoConnectionScreen();

  default:
    const exhaustiveCheck: never = callingState;
    throw new Error(`Unknown calling state: ${exhaustiveCheck}`);
}