Subscribing to streams — Web

Once you have connected to a session, you can subscribe to streams in the session. When you subscribe to a stream, its video stream appears in the client page and its audio is played.

This topic includes the following sections:

Detecting when streams are created in a session

The Session object dispatches a streamCreated event when a new stream (other than your own) is created in a session. A stream is created when a client publishes a stream to the session. The streamCreated event is also dispatched for each existing stream in the session when you first connect. This event is defined by the StreamEvent, which has a stream property, representing stream that was created:

session.on("streamCreated", function (event) {
   console.log("New stream in the session: " + event.stream.streamId);
});
// Replace with a valid token:
session.connect(token);

You can subscribe to any stream. See the next section.

Subscribing to a stream

To subscribe to a stream, pass the Stream object into the subscribe method of the Session object:

session.subscribe(stream, replacementElementId);

The subscribe() method takes the following parameters:

  • stream—The Stream object.

  • targetElement— (Optional) Defines the DOM element that the Subscriber video replaces.

  • properties— (Optional) A set of properties that customize the appearance of the Subscriber view in the HTML page (see Customizing the UI) and select whether to subscribe to audio and video (see Adjusting audio and video).

  • completionHandler— (Optional) A function that is called asynchronously when the call to the subscribe() method completes successfully or fails. If the call to the subscribe() method fails, the completion handler is passed an error object. This object has a code and message property that describe the error.

The following code subscribes to all streams, other than those published by your client:

session.on("streamCreated", function(event) {
    session.subscribe(event.stream);
});

// Replace with your API key and token:
session.connect(token, function (error) {
    if(error) {
        // failed to connect
    }
});

The insertMode property of the properties parameter of the Session.subscribe() method specifies how the Publisher object will be inserted in the HTML DOM, in relation to the targetElement parameter. You can set this parameter to one of the following values:

  • "replace" — The Subscriber object replaces contents of the targetElement. This is the default.
  • "after" — The Subscriber object is a new element inserted after the targetElement in the HTML DOM. (Both the Subscriber and targetElement have the same parent element.)
  • "before" — The Subscriber object is a new element inserted before the targetElement in the HTML DOM. (Both the Subscriber and targetElement have the same parent element.)
  • "append" — The Subscriber object is a new element added as a child of the targetElement. If there are other child elements, the Publisher is appended as the last child element of the targetElement.

For example, the following code adds a new Subscriber object as a child of a subscriberContainer DOM element:

session.on('streamCreated', function(event) {
  var subscriberProperties = {insertMode: 'append'};
  var subscriber = session.subscribe(event.stream,
    'subscriberContainer',
    subscriberProperties,
    function (error) {
      if (error) {
        console.log(error);
      } else {
        console.log('Subscriber added.');
      }
  });
});

The Subscriber object has an element property, which is set to the HTML DOM element containing it.

If you do not want to use the default UI, you access the Video element for the Subscriber (see this topic). You can also use your own Video element to display the Subscriber video, and use the Subscriber's MediaStream object as the media source for that Video element (see this topic).

Unsubscribing from a stream

To stop playing a stream you are subscribed to, pass the Subscriber object into the unsubscribe() method of the Session object:

session.unsubscribe(subscriber);

The Subscriber object is destroyed, and the stream display is removed from the HTML DOM.

Detecting when streams leave a session

When a stream, other than your own, leaves a session the Session object dispatches a streamDestroyed event:

session.on("streamDestroyed", function (event) {
  console.log("Stream stopped. Reason: " + event.reason);
});

When a stream you publish leaves a session the Publisher object dispatches a streamDestroyed event:

var publisher = OT.initPublisher();
publisher.on("streamDestroyed", function (event) {
  console.log("Stream stopped. Reason: " + event.reason);
});

The streamDestroyed event is defined by the StreamEvent class. The event includes a reason property, which details why the stream ended. These reasons include "clientDisconnected", "forceDisconnected", "forceUnpublished", or "networkDisconnected". For details, see StreamEvent.

By default, when a streamDestroyed event is dispatched for a stream you are subscribed to, the corresponding Subscriber objects (there could be more than one) are destroyed and removed from the HTML DOM. You can prevent this default behavior by calling the preventDefault() method of the StreamEvent object:

session.on("streamDestroyed", function (event) {
  event.preventDefault();
  var subscribers = session.getSubscribersForStream(event.stream);
  // Now you can adjust the DOM elements around each
  // subscriber to the stream, and then delete it yourself.
});

Note that the getSubscribersForStream() method of a Session object returns all of the Subscriber objects for a Stream.

You may want to prevent the default behavior, and retain the Subscriber, if you want to adjust related DOM elements before deleting the Subscriber yourself. You can then delete the Subscriber object (and its DOM element) by calling the destroy() method of the Subscriber object.

A Subscriber object dispatches a destroyed event when the object has been removed from the HTML DOM. In response to this event, you may choose to adjust (or remove) DOM elements related to the subscriber that was removed.

Automatic reconnection

If a client drops a connection to a subscribed stream (for example, due to a drop in network connectivity in either client), it will attempt to automatically reconnect to the stream. When the stream is dropped and the client tries to reconnect, the Subscriber object dispatches a disconnected event. When the stream is restored, the Subscriber object dispatches a connected event. If the client cannot restore the stream, the Subscriber object dispatched a destroyed event.

In response to these events, your application can (optionally) display user interface notifications indicating the temporary disconnection, reconnection, and destroyed states:

subscriber.on(
  disconnected: function() {
    // Display a user interface notification.
  },
  connected: function() {
    // Adjust user interface.
  },
  destroyed: function() {
    // Adjust user interface.
  }
);

Restricting the frame rate of a subscribed stream

You can also restrict the frame rate of a Subscriber's video stream. To restrict the frame rate of a subscriber, call the restrictFrameRate() method of the Subscriber object, passing in true:

subscriber.restrictFrameRate(true);

Pass in false and the frame rate of the video stream is not restricted:

subscriber.restrictFrameRate(false);

When the frame rate is restricted, the Subscriber video frame will update once or less per second.

This feature is only available in sessions that use the OpenTok Media Router (sessions with the media mode set to routed), not in sessions with the media mode set to relayed. In relayed sessions, calling this method has no effect.

Restricting the subscriber frame rate has the following benefits:

  • It reduces CPU usage.
  • It reduces the network bandwidth consumed by the app.
  • It lets you subscribe to more streams simultaneously.

Reducing a subscriber's frame rate has no effect on the frame rate of the video in other clients.

Detecting when a subscriber's audio is blocked or unblocked

Some browsers automatically block audio playback, requiring a click event before audio playback starts for subscribers. These browsers include Safari, Firefox 66+, and Chrome 71+.

The Subscriber object displays an audio playback button if audio playback is blocked. You can disable the Subscriber's default audio playback button and display your own UI element that the user will click to start audio playback. See Displaying a custom UI element when Subscriber audio is blocked.

When the subscriber's audio is blocked, the Subscriber object dispatches a audioBlocked event, and it dispatches an audioUnblocked event when the audio is unblocked:

subscriber.on({
  audioBlocked: function(event) {
   console.log("Subscriber audio is blocked.")
  },
  audioUnblocked: function(event) {
   console.log("Subscriber audio is unblocked.")
  }
});

Also, the Subscriber includes an isAudioBlocked() which returns true if the audio is blocked or false if it is not.

Subscriber audio is unblocked when any of the following occurs:

  • The user clicks the default Subscriber audio playback icon
  • The OT.unblockAudio() method is called in response to an HTML element dispatching a click event (if you have disabled the default audio playback icon)
  • The local client gains access to the camera or microphone (for instance, in response to a successful call to OT.initPublisher()).

For more information, see this Mozilla article about autoplay in Firefox and this Google article about autoplay in Chrome.

Detecting when a subscriber's video is disabled

When the subscriber's video is disabled, the Subscriber object dispatches a videoDisabled event:

subscriber.on("videoDisabled", function(event) {
  // You may want to hide the subscriber video element:
  domElement = document.getElementById(subscriber.id);
  domElement.style["visibility"] = "hidden";

  // You may want to add or adjust other UI.
});

When the OpenTok Media Router, or a fallback-enabled publisher, disables the video of a subscriber, you may want to adjust the user interface related to the subscriber.

The reason property of the videoDisabled event object defines the reason the video was disabled. This can be set to one of the following values:

  • "publishVideo" — The publisher stopped publishing video by calling publishVideo(false).

  • "quality" — The OpenTok Media Router, or the publishing client if publisher audio fallback is enabled, stopped sending video to the subscriber based on stream quality changes. This feature of the OpenTok Media Router has a subscriber drop the video stream when connectivity degrades. (The subscriber continues to receive the audio stream, if there is one.) The publisher audio fallback feature has the publisher stop publishing the video stream when publisher connectivity degrades, and subsequently the subscriber drops the video stream.

    Before sending this event, when the Subscriber's stream quality deteriorates, or a fallback-enabled publisher's stream quality deteriorates, to a level that is low enough that the video stream is at risk of being disabled, the Subscriber dispatches a videoDisableWarning event.

    If connectivity improves to support video again, the Subscriber object dispatches a videoEnabled event, and the Subscriber resumes receiving video.

    By default, the Subscriber displays a video disabled indicator when a videoDisabled event with this reason is dispatched and removes the indicator when the videoDisabled event with this reason is dispatched. You can control the display of this icon by calling the setStyle() method of the Subscriber, setting the videoDisabledDisplayMode property; or you can set the style when calling the Session.subscribe() method, setting the style property of the properties parameter.

    This feature is only available in sessions that use the OpenTok Media Router (sessions with the media mode set to routed), or in sessions with a fallback-enabled publisher. See the publisher fallback enabled docs.

    When you publish a stream, you can prevent it from having its video disabled due to stream quality. Set audioFallbackEnabled to false in the properties object passed into the OT.initPublisher() method (this feature will be deprecated), or set subscriber to false in the audioFallback object passed in as the properties parameter of the OT.initPublisher() method.

  • "subscribeToVideo" — The subscriber started or stopped subscribing to video, by calling subscribeToVideo(false).

  • "codecNotSupported" — The subscriber stopped subscribing to video due to an incompatible codec (see the Video codecs developer guide).

The Subscriber dispatches a videoEnabled event when video resumes:

subscriber.on("videoEnabled", function(event) {
  // You may want to display the subscriber video element,
  // if it was hidden:
  domElement = document.getElementById(subscriber.id);
  domElement.style["visibility"] = "visible";

  // You may want to add or adjust other UI.
});

The reason property of the videoEnabled event object defines the reason the video was enabled. This can be set to one of the following values:

  • "publishVideo" — The publisher started publishing video by calling publishVideo(true).

  • "quality" — The OpenTok Media Router, or the fallback-enabled publisher, resumed sending video to the subscriber based on stream quality changes. This feature of the OpenTok Media Router has a subscriber drop the video stream when connectivity degrades and then resume the video stream if the stream quality improves. The publisher audio fallback feature has the publisher stop publishing the video stream when publisher connectivity degrades, and subsequently the subscriber drops the video stream.

    This feature is only available in sessions that use the OpenTok Media Router (sessions with the media mode set to routed), or in sessions with a fallback-enabled publisher.

  • "subscribeToVideo" — The subscriber started or stopped subscribing to video, by calling subscribeToVideo(false).

  • "codecChanged" — The subscriber video was enabled after a codec change from an incompatible codec (see the Video codecs developer guide).

Detecting when a subscriber's stream's video dimensions change

The stream of a subscriber's video dimensions can change if a stream published from a mobile device resizes, based on a change in the device orientation. It can also occur if the video source is a screen-sharing window and the user publishing the stream resizes the window that is the source for the stream. When the video dimensions change, the Subscriber object dispatches a videoDimensionsChanged event.

The following code resizes a subscriber when the stream's video dimensions change:

subscriber.on('videoDimensionsChanged', function(event) {
  subscriber.element.style.width = event.newValue.width + 'px';
  subscriber.element.style.height = event.newValue.height + 'px';
  // You may want to adjust other UI.
});

Getting information about a stream

The Stream object has the following properties that define the stream:

  • connection—The Connection object corresponding to the connection that is publishing the stream. You can compare this to the connection property of the Session object to see if the stream is being published by the local web page.
  • creationTime—The timestamp (a number) for the creation of the stream. This value is calculated in milliseconds. You can convert this value to a Date object by calling new Date(stream.creationTime).
  • hasAudio—(Boolean) Whether the stream has audio. This property can change if the publisher turns on or off audio (by calling Publisher.publishAudio()). When this occurs, the Session object dispatches a streamPropertyChanged event.
  • hasVideo—(Boolean) Whether the stream has video.
  • initials—(Boolean) The initials for the stream (if initials were set when the stream's publisher was initialized).
  • name—(String) The name of the stream. This is, by default, displayed when the user mouses over the Subscriber in the HTML DOM. You can, however, customize the UI to hide the name or display it without mousing over.
  • videoDimensions—This object has two properties: width and height. Both are numbers. The width property is the width of the encoded stream; the height property is the height of the encoded stream. (These are independent of the actual width of Publisher and Subscriber objects corresponding to the stream.) This property can change if a stream published from an iOS device resizes, based on a change in the device orientation.
  • videoType—The type of video: either "camera", "screen", "custom", or undefined. A "screen" video uses screen sharing on the publisher as the video source; a "custom" video uses a VideoTrack element as the video source on the publisher. The videoType is undefined when a stream is voice-only (see the Voice-only guide). This property can change if a stream published from a mobile device changes from a camera to a screen-sharing video type. For more information, see Screen sharing — Web.

The hasAudio, hasVideo, videoDimensions, and videoType properties can change (for example, when the publisher turns on or off video). When this occurs, the Session object dispatches a streamPropertyChanged event (see StreamPropertyChangedEvent.)

The getStats() method of a Subscriber object provides you with information about the subscriber's stream. To get low-level peer connection statistics, use the Subscriber.getRtcStatsReport() method. It returns a promise that, on success, resolves with an RtcStatsReport object for the subscribed stream.

Refer to the client observability developer guide for detailed information.

Setting the preferred frame rate and resolution

When subscribing to a stream that uses the scalable video feature, you have an option to set preferredResolution to "auto" to automatically manage Subscriber video resolution based on the size being rendered to optimize the network and CPU usage. For advanced users, you can also manually set the preferred frame rate and resolution for the stream the subscribing client receives from the OpenTok Media Router. You can set these as the preferredFrameRate and preferredResolution properties of the options you pass into the [`Session.subscribe()`](/video/sdk-reference/js/Session.html#subscribe) method. We recommend setting preferredResolution to "auto". With the "auto" setting, OpenTok.js selects the preferred resolution based on the dimensions of the Subscriber video in the browser. You can also set the preferred frame rate and resolution after subscribing to a stream (see [`Subscriber.setPreferredFrameRate()`](/opentok/sdks/js/reference/Subscriber.html#setPreferredFrameRate) and Subscriber.setPreferredResolution()).

Note: The "auto" resolution setting only applies when you use the default Subscriber Video element created by the SDK. It does not work if you create your own Video element in response to the videoElementCreated event (see this topic).

Note: These preferences assume the publisher is using the default scalability layer layout. If the publisher has set a non-default target scalability mode (see Setting the target scalability mode), the Media Router's layer selection may not match the requested resolution or frame rate. See Interaction with subscriber preferred resolution and frame rate for details.

Applying filters and effects to subscribed audio and video

You can apply filters and effects on audio or video tracks for a subscribed stream — see this topic.

Detecting audio and video quality changes

If a client experiences periods of degraded network connectivity, this may be reflected on the subscriber call quality. The Subscriber object dispatches a qualityScoreChanged event when the calculated audio and video MOS scores change. These scores are reported as integers between 1 (worst) and 5 (best), corresponding to bad, poor, fair, good, and excellent. For more details, see the Subscriber qualityScoreChanged event.

A Subscriber object dispatches this event only when one of the quality scores has changed. Each Subscribe dispatches events with its own audio and video quality scores, depending on whether it is subscribing to audio, video, or both.

In response to these events, your application can (optionally) notify the client of network conditions resulting in degraded call quality:

subscriber.on('qualityScoreChanged', ({qualityScore}) => {
  if (qualityScore.audioQualityScore <= 3){
    // Alert the user that the remote party is experiencing degraded service
  }
  if (qualityScore.videoQualityScore <= 3){
    // Alert the user that the remote party is experiencing degraded service
  }
});

Troubleshooting

Follow the tips in this section to avoid connectivity issues when subscribing. For general information on troubleshooting, see Debugging — Web.

Handling Errors

Handling errors when subscribing is a bit easier than when publishing. There is only one way to subscribe—with the Session.subscribe() method—and pretty much any error that happens when subscribing comes down to a network issue. This can happen if, for example, the user is on a really restrictive network connection that does not allow for WebRTC connections (but the WebSocket connection worked). If the Subscriber fails to connect it will just display its own error message inside the Subscriber. It doesn't look particularly nice and isn't very informative to the end user. We recommend that you handle this case yourself and surface a message to the user indicating that they failed to subscribe and that they should check their network connection. Handling these errors looks like this:

session.subscribe(event.stream, 'subscriber', {insertMode: 'append'}, function (err) {
  if (err) {
    showMessage('Streaming connection failed. This could be due to a restrictive firewall.');
  }
});

Losing Connectivity

Your Subscriber can also lose its connection after it has already succeeded in connecting. More often than not, this will also result in the Session losing its connection, but that's not always the case. Also, it may be that the Publisher on the other side has lost its connection rather than the connection being lost locally. You can handle the Subscriber disconnecting by listening for the streamDestroyed event on the Session with a reason property set to "networkDisconnected", like this:

session.on({
  streamDestroyed: function (event) {
    if (event.reason === 'networkDisconnected') {
      event.preventDefault();
      var subscribers = session.getSubscribersForStream(event.stream);
      if (subscribers.length > 0) {
        var subscriber = document.getElementById(subscribers[0].id);
        // Display error message inside the Subscriber
        subscriber.innerHTML = 'Lost connection. This could be due to your internet connection '
          + 'or because the other party lost their connection.';
        event.preventDefault();   // Prevent the Subscriber from being removed
      }
    }
  }
});

Implementing Session Subscribe Retries

Transient subscription failures can occur when session.subscribe() is called and the underlying WebRTC connection cannot be established in time, or when a network blip interrupts ICE negotiation. When session.subscribe() fails, the SDK returns an error via its completion handler callback. The recommended approach is to implement application-level retry logic with a delay between attempts.

Note: Built-in retry support for session.subscribe() is on the SDK roadmap. Until that ships, you need to implement this yourself.

Why Subscribe Failures Happen

The most common root causes for transient subscription failures are:

  • OT_TIMEOUT (code 1501): The subscription did not complete within the allowed time window (30 seconds). This is the subscribe-side equivalent of the publish timeout and is the most common retryable error.
  • ICE negotiation failures (OT_ICE_WORKFLOW_FAILED): The WebRTC peer connection could not be established, typically due to a restrictive network or a transient connectivity issue.
  • Peer connection creation failures (OT_CREATE_PEER_CONNECTION_FAILED): The WebRTC peer connection object could not be created, often caused by a temporary platform or network issue.
  • Network blips during the subscription flow: A brief network interruption during ICE negotiation or media binding can cause the subscription to time out without a hard error.

Recoverable vs. Non-Recoverable Errors

Not all session.subscribe() errors are equal. Classifying errors correctly before retrying is essential — retrying on a non-recoverable error wastes time and can mask real failures.

Note: Always use the error.name property to identify errors programmatically. The numeric error.code property is deprecated.

Non-Recoverable Errors — Do Not Retry

These errors represent hard constraints, invalid call context, or terminal stream states. Retrying will not resolve them.

error.name Description Recommended Action
OT_NOT_CONNECTED session.subscribe() was called before the session was connected. Ensure session.connect() has completed successfully before subscribing.
OT_DISCONNECTED The action failed because the client is not connected to the session. Wait for the session to reconnect before retrying.
OT_INVALID_PARAMETER One or more parameters passed to session.subscribe() were invalid (e.g. null stream or target element). Fix the application logic. Do not retry.
OT_STREAM_DESTROYED The stream was destroyed before it could be subscribed to. Do not retry — the stream no longer exists. Remove any pending subscription state for this stream.
OT_STREAM_NOT_FOUND The stream could not be found in the session. Do not retry — the stream is no longer available.
OT_STREAM_LIMIT_EXCEEDED The session has exceeded the limit for simultaneous streams. Inform the user. Do not retry until a stream slot becomes available.
OT_UNABLE_TO_SUBSCRIBE The user attempted to subscribe in an E2EE-enabled session without specifying an encryption secret; or an unexpected error prevented subscription. For E2EE sessions, ensure an encryption secret is set via session.setEncryptionSecret() before subscribing. For the generic case, log the error and inform the user.

Recoverable Errors — Safe to Retry

These errors are typically caused by transient network conditions, signalling timeouts, or temporary platform unavailability.

error.name Description Recommended Action
OT_TIMEOUT (code 1501) The subscription did not complete in a reasonable amount of time. The most common retryable subscribe error. Unsubscribe, then retry with backoff (up to 3 attempts).
OT_ICE_WORKFLOW_FAILED ICE negotiation failed — the peer connection could not be established. Often transient on restrictive networks. Unsubscribe, then retry. If it persists after all attempts, inform the user of a potential network/firewall issue.
OT_CREATE_PEER_CONNECTION_FAILED The WebRTC peer connection could not be created. May indicate a restrictive firewall or temporary platform issue. Unsubscribe, then retry. If it persists, suggest the user check their network connection.
OT_SET_REMOTE_DESCRIPTION_FAILED The WebRTC connection failed during setRemoteDescription. Typically a transient signalling issue. Unsubscribe, then retry with backoff.
OT_MEDIA_ERR_ABORTED / OT_MEDIA_ERR_NETWORK Media acquisition was aborted or interrupted by a network error. Unsubscribe, then retry after a short delay.
OT_MEDIA_ERR_DECODE A decoding error occurred while trying to play the stream in the video element. Unsubscribe, then retry after a short delay. If it persists, the media format may be incompatible.
OT_MEDIA_ERR_SRC_NOT_SUPPORTED The stream has been detected as not suitable for playback. Unsubscribe, then retry once. If it persists, check the subscriber's video element configuration.

Important: Always Unsubscribe Before Retrying

Unlike session.publish(), where the publisher instance can often be reused directly, session.subscribe() requires you to call session.unsubscribe() and discard the subscriber object before retrying. Attempting to reuse a failed subscriber instance will not work.

async function subscribeWithRetry(session, stream, targetElement, options, attempt = 1) {
  const MAX_RETRIES = 3;
  const RETRY_DELAY_MS = 3000;

  let subscriber = session.subscribe(stream, targetElement, options);

  const error = await new Promise((resolve) => {
    subscriber.on('subscribeComplete', (err) => resolve(err));
  });

  if (!error) {
    console.log('Subscribed successfully.');
    return subscriber;
  }

  // Always clean up the failed subscriber before retrying
  try { session.unsubscribe(subscriber); } catch (e) { /* ignore */ }

  // Non-recoverable: do not retry
  const nonRetryable = [
    'OT_NOT_CONNECTED',
    'OT_DISCONNECTED',
    'OT_INVALID_PARAMETER',
    'OT_STREAM_DESTROYED',
    'OT_STREAM_NOT_FOUND',
    'OT_STREAM_LIMIT_EXCEEDED',
    'OT_UNABLE_TO_SUBSCRIBE',
  ];

  if (nonRetryable.includes(error.name)) {
    console.error('Non-retryable subscribe error:', error.name);
    handleNonRecoverableError(error);
    return null;
  }

  // Recoverable: retry with backoff
  if (attempt < MAX_RETRIES) {
    console.warn(`Subscribe attempt ${attempt} failed (${error.name}), retrying...`);
    await delay(RETRY_DELAY_MS * attempt);
    return subscribeWithRetry(session, stream, targetElement, options, attempt + 1);
  }

  console.error('All subscribe attempts failed.');
  handleSubscribeFailure(session, stream);
  return null;
}

function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

function handleNonRecoverableError(error) {
  // Surface a meaningful message to the user based on error.name
}

function handleSubscribeFailure(session, stream) {
  // Inform the user that the stream could not be loaded
}

Usage:

session.on('streamCreated', (event) => {
  subscribeWithRetry(session, event.stream, document.getElementById('subscriber'), {});
});

Timing-Sensitive Scenarios

Stream Ends During a Retry Attempt

If the stream is destroyed while a retry is pending, the streamDestroyed session event will fire. You must cancel any pending retry for that stream to avoid subscribing to a stream that no longer exists.

const pendingRetries = new Map(); // stream.id → timeout handle

session.on('streamDestroyed', (event) => {
  const pending = pendingRetries.get(event.stream.id);
  if (pending) {
    clearTimeout(pending);
    pendingRetries.delete(event.stream.id);
    console.log(`Cancelled pending retry for destroyed stream: ${event.stream.id}`);
  }
});

Session Reconnecting During a Subscribe Retry

If the session is reconnecting (e.g. after a network drop), defer the retry until the session has reconnected. Attempting to subscribe while the session is reconnecting will fail immediately.

let isSessionReconnecting = false;

session.on('sessionReconnecting', () => { isSessionReconnecting = true; });
session.on('sessionReconnected', () => {
  isSessionReconnecting = false;
  // Re-trigger any deferred subscriptions here
});

// In your retry logic, check before retrying:
if (isSessionReconnecting) {
  // Defer — wait for sessionReconnected before retrying
  return;
}

What NOT to Do

  • Do not reuse a failed subscriber instance — always call session.unsubscribe() and create a new subscription on retry.
  • Do not retry on OT_STREAM_DESTROYED or OT_STREAM_NOT_FOUND — the stream is gone and retrying will always fail.
  • Do not retry on OT_STREAM_LIMIT_EXCEEDED — this is a session-level capacity constraint, not a transient error.
  • Do not retry indefinitely — cap at 3 attempts and inform the user if all fail.
  • Do not retry while the session is reconnecting — defer until sessionReconnected fires.
Parameter Recommended Value Notes
Max retries 3 Consistent with session.publish() retry guidance
Retry delay 3s × attempt (3s, 6s, 9s) Slightly longer than publish retries — subscription timeout is 30s
On all retries fail Inform user Avoid silently dropping the stream
Non-retryable errors OT_STREAM_DESTROYED, OT_STREAM_NOT_FOUND, OT_STREAM_LIMIT_EXCEEDED Fail fast on these
Subscriber cleanup Always session.unsubscribe() before retry Required — unlike publishers, subscriber instances cannot be reused