Publishing streams — Web

Once you have connected to a session, you can publish a stream that other clients connected to the session can view.

This topic includes the following sections:

Checking whether a client has publish capabilities

Once you have connected to a session, you can check if the client can publish. Check the value of the capabilities.publish property of the Session object. If it is set to 1, the client can publish:

if (session.capabilities.publish == 1) {
    // The client can publish. See the next section.
} else {
    // The client cannot publish.
    // You may want to notify the user.
}

To publish, the client must connect to the session with a token that is assigned a role that supports publishing. There must be a connected camera and microphone. Also, the client environment must support publishing (see Browser support).

Also, publishing is only supported on HTTPS pages.

Initializing a Publisher

The OT.initPublisher() method initializes and returns a Publisher object. The Publisher object represents the view of a video you publish:

var publisher;
var targetElement = 'publisherContainer';

publisher = OT.initPublisher(targetElement, null, function(error) {
  if (error) {
    // The client cannot publish.
    // You may want to notify the user.
  } else {
    console.log('Publisher initialized.');
  }
});

The OT.initPublisher() method takes three parameters:

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

  • properties— (Optional) A set of properties that customize the Publisher. The properties parameter also includes options to specify an audio and video input device used by the publisher (see Setting the camera and microphone used by the publisher). The properties parameter also includes options for customizing the appearance of view in the HTML page (see Customizing the UI) and select whether to publish audio and video (see Publishing audio or video only). For more publisher options, see the documentation of the properties parameter of the OT.initPublisher() method.

  • completionHandler— (Optional) A completion handler that specifies whether the publisher instantiated successfully or with an error.

You can pass this Publisher object into the Session.publish() method to publish a stream to a session. See Publishing a stream.

Before calling Session.publish(), you can use this Publisher object to test the microphone and camera attached to the Publisher.

The insertMode property of the properties parameter of the OT.initPublisher() 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 Publisher object replaces contents of the targetElement. This is the default.
  • "after" — The Publisher object is a new element inserted after the targetElement in the HTML DOM. (Both the Publisher and targetElement have the same parent element.)
  • "before" — The Publisher object is a new element inserted before the targetElement in the HTML DOM. (Both the Publisher and targetElement have the same parent element.)
  • "append" — The Publisher 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 Publisher object as a child of a publisherContainer DOM element:

// Try setting insertMode to other values: "replace", "after", or "before":
var publisherProperties = {insertMode: "append"};
var publisher = OT.initPublisher('publisherContainer', publisherProperties, function (error) {
  if (error) {
    console.log(error);
  } else {
    console.log("Publisher initialized.");
  }
});

Detecting when a client has granted access to the camera and microphone

Before a Publisher object can access the client's camera and microphone, the user must grant access to them. The Publisher object dispatches events when the user grants or denies access to the camera and microphone:

publisher.on({
  accessAllowed: function (event) {
    // The user has granted access to the camera and mic.
  },
  accessDenied: function accessDeniedHandler(event) {
    // The user has denied access to the camera and mic.
  }
});

Also, a Publisher object dispatches events when the user is presented with the option to allow or deny access to the camera and microphone:

publisher.on({
  accessDialogOpened: function (event) {
    // The Allow/Deny dialog box is opened.
  },
  accessDialogClosed, function (event) {
    // The Allow/Deny dialog box is closed.
  }
});

The Publisher has an accessAllowed property, which indicates whether a client has (true) or has not (false) granted access to the camera and microphone.

Setting the camera and microphone used by the publisher

You can (optionally) specify an audio and video input device for the publisher to use. When you call the OT.initPublisher() method, you can (optionally) set the audioSource and videoSource properties of the properties object passed into the OT.initPublisher() method.

First, use the OT.getDevices() method to enumerate available devices. The array of devices is passed in as the devices parameter of the callback function passed into the OT.getDevices() method. For example, the following code gets a list of audio and video input devices:

var audioInputDevices;
var videoInputDevices;
OT.getDevices(function(error, devices) {
  audioInputDevices = devices.filter(function(element) {
    return element.kind == "audioInput";
  });
  videoInputDevices = devices.filter(function(element) {
    return element.kind == "videoInput";
  });
  for (var i = 0; i < audioInputDevices.length; i++) {
    console.log("audio input device: ", audioInputDevices[i].deviceId);
  }
  for (i = 0; i < videoInputDevices.length; i++) {
    console.log("video input device: ", videoInputDevices[i].deviceId);
  }
});

Each device listed by OT.getDevices() has a unique device ID, set as the deviceId property. You can use these device ID values as the audioSource and videoSource properties of the properties object passed into OT.initPublisher():

var pubOptions =
  {
    audioSource: audioInputDevices[0].deviceId,
    videoSource: videoInputDevices[0].deviceId
  };
var publisher = OT.initPublisher(null, pubOptions, function(error) {
  console.log("OT.initPublisher error: ", error);
});

Set the videoSource property to null or false in a voice-only session (see Publishing in a voice session).

The OpenTok hardware set-up component provides a user interface for clients to select the camera and microphone to use. It is built using the OT.getDevices() method.

Note that you can also publish a screen-sharing stream — one in which the source is the client's screen, not a camera. For details, see Screen sharing.

You can also change the camera used by the publisher, or set it to use the front- or back-facing camera (when this option is available).

You can also change the audio source used by the publisher.

Using the front- or back-facing camera

When you initialize a publisher, you can set the facingMode property of the options object you pass into the OT.initPublisher(). For example, you can set the property to "user" (front-facing camera) or "environment" (rear-facing camera), when this option is available on the client's system. (Generally, these options are available on mobile devices only.)

If you set the facingMode option, do not set the videoSource property.

Remembering the camera and microphone selection

For security in pages loaded over HTTP, all browsers always prompt the user to select the camera and microphone used to publish a stream.

In pages loaded over HTTPS in Chrome, the user's camera and microphone selection is remembered and reused on subsequent visits to a page loaded from the same HTTPS domain.

In pages loaded over HTTPS in Firefox, the user has an option to remember the camera and microphone (in subsequent visits to a page loaded from the same HTTPS domain) when selecting the devices.

In pages loaded over HTTPS in IE, you can use the user's previous camera and microphone selection from previous usage to the same HTTPS domain (if there was any), by setting the usePreviousDeviceSelection property to true in the options you pass into the OT.initPublisher() method:

var pubOptions = {usePreviousDeviceSelection: true};
var publisher = OT.initPublisher(null, pubOptions, function(error) {
  console.log("OT.initPublisher error: ", error);
});

To prompt the user to select the camera and microphone to use in IE (and ignore previous device selections), do not set the usePreviousDevices property in the options you pass into the OT.initPublisher() method (or set it to false, the default).

Disabling default audio input device management

By default, the SDK automatically handles the audio input device switching if there was a new one plugged in. This might not be the desired behavior for some end users that would like to keep the selection of their current microphone.

As an advanced user of the SDK, you can disable automatic audio input device management. You can do so by setting the disableAudioInputDeviceManagement property to the options passed into the OT.initPublisher() method:

var pubOptions = {disableAudioInputDeviceManagement: true};
var publisher = OT.initPublisher(null, pubOptions, function(error) {
  console.log("Publishing a stream");
});

Note: This is an advanced feature. If you enable this, the audio input device used by the SDK will not be updated when the end user changes their microphone.

Publishing a stream

Once you create a Publisher object (See Initializing a publisher), you can pass it into the publish() method of a Session object to publish a stream to the session:

    publisher = OT.initPublisher('replacementElementId');
    session.publish(publisher, function(error) {
      if (error) {
        console.log(error);
      } else {
        console.log('Publishing a stream.');
      }
    });

The second parameter is a completion handler function that is passed an error object if publishing fails. Otherwise the completion handler function is called with no error passed in.

This code assumes that session is a Session object, and that the client has connected to the session. For more information, see Joining a Session.

The Publish object dispatches a streamCreated event when it starts streaming to the session:

var publisher = OT.initPublisher();
session.publish(publisher, function(error) {
  if (error) {
    console.log(error);
  } else {
    console.log('Publishing a stream.');
  }
});
publisher.on('streamCreated', function (event) {
    console.log('The publisher started streaming.');
});

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

Stopping a publisher from streaming to a session

You can stop publisher from streaming to the session by calling the unpublish() method of the Session object:

    session.unpublish(publisher);

Note that you can individually stop sending video or audio (while still publishing). For more information, see Adjusting audio and video.

Detecting when a published stream leaves a session

The Publisher object dispatches a streamDestroyed event when it stops streaming to the session:

var publisher = OT.initPublisher();
session.publish(publisher);
publisher.on("streamDestroyed", function (event) {
  console.log("The publisher stopped streaming. 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 Publisher dispatches the streamDestroyed event, the Publisher is destroyed and removed from the HTML DOM. You can prevent this default behavior by calling the preventDefault() method of the StreamEvent object:

publisher.on("streamDestroyed", function (event) {
    event.preventDefault();
    console.log("The publisher stopped streaming.");
});

You may want to prevent the default behavior, and retain the Publisher, if you want to reuse the Publisher object to publish again to the session.

The Publisher also 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 publisher that was removed.

Setting the video resolution of a stream

To set a recommended video resolution for a published stream, set the resolution property of the properties parameter you pass into the OT.initPublisher() method:

var publisherProperties = {resolution: '1280x720'};
var publisher = OT.initPublisher(targetElement,
                                 publisherProperties);
publisher.on('streamCreated', function(event) {
   console.log('Stream resolution: ' +
     event.stream.videoDimensions.width +
     'x' + event.stream.videoDimensions.height);
});

This resolution property is a string, defining the desired resolution of the video. The format of the string is "_width_x_height_", where the width and height are represented in pixels. Valid values are "1920x1080", "1280x720", "640x480", and "320x240".

The requested resolution of a video stream is set as the videoDimensions.width and videoDimensions.height properties of the Stream object.

The default resolution for a stream (if you do not specify a resolution) is 640x480 pixels. If the client system cannot support the resolution you requested, the stream will use the next largest setting supported.

The videoHeight() and videoWidth() methods return the configured resolution of the Publisher object. The actual resolution of a Subscriber video stream is returned by the videoWidth() and videoHeight() methods of the Subscriber object. These may differ from the values of the resolution property passed in as the properties property of the OT.initPublisher() method, if publishing the browser does not support the requested resolution.

Note: See the 1080p developer guide for considerations about using 1080p resolution.

Setting the frame rate of a stream

To set a recommended frame rate for a published stream, set the frameRate property of the properties parameter you pass into the OT.initPublisher() method:

var publisherProperties = {frameRate: 7};
var publisher = OT.initPublisher(targetElement,
                                 publisherProperties);
publisher.on('streamCreated', function(event) {
   console.log('Frame rate: ' + event.stream.frameRate);
});

Set the value to the desired frame rate, in frames per second, of the video. Valid values are 30, 15, 7, and 1.

If the publisher specifies a frame rate, the actual frame rate of the video stream is set as the frameRate property of the Stream object, though the actual frame rate will vary based on changing network and system conditions. If you do not specify a frame rate when you call OT.initPublisher, this property is undefined.

For sessions that use the OpenTok Media Router (sessions with the media mode set to routed), lowering the frame rate proportionally reduces the maximum bandwidth the stream can use. However, in session with the media mode set to relayed, lowering the frame rate does not reduce the stream's bandwidth.

You can also restrict the frame rate of a Subscriber's video stream. For more information, see Restricting the frame rate of a subscribed stream.

Setting the maximum bitrate for a stream

You can set the maximum bitrate for a published stream. Setting the maximum bitrate can help to reduce bandwidth consumption when a user is connecting from a metered connection. See this documentation.

Deleting a Publisher

You can delete a Publisher by calling its destroy() method:

    publisher.destroy();

Calling the destroy() method deletes the Publisher object and removes it from the HTML DOM.

Getting statistics about a publisher's stream

The Publisher.getStats() method provides you with an array of objects defining the current audio-video statistics for the publisher. For a publisher in a routed session (one that uses the OpenTok Media Router), this array includes one object, defining the statistics for the single audio-video stream that is sent to the OpenTok Media Router. In a relayed session, the array includes an object for each subscriber to the published stream.

To get detailed low-level peer connection statistics, use the Publisher.getRtcStatsReport() method. It returns a promise that, on success, resolves with an array of RtcStatsReport objects.

Refer to the client observability developer guide for detailed information.

Testing a publisher's stream

You can publish a test stream and check its audio and video statistics to determine the type of stream (such as high-resolution or audio-only) supported by your connection.

To get statistics for a stream published by the local client, you must use a session that uses the OpenTok Media Router (sessions with the media mode set to routed), and you must set the testNetwork property to true in the options object you pass into the Session.subscribe() method. You can then use the getStats() method of the Subscriber object to get audio and video statistics for the stream you publish. See this topic for more information.

The opentok-network-test repo includes sample code for showing how to use statistics of a test stream before publishing to a session.

Publishing video from a video source other than a camera or screen

You can set the video source for a Publisher to a video MediaStreamTrack object. This lets you do the following:

  • Publish video using an HTML Canvas element as the video. You can call the captureStream() method of the HTMLCanvasElement object and call the getVideoTracks() method of the resulting CanvasCaptureMediaStream object to get a video MediaStreamTrack object. For a basic example, see the Publish-Canvas sample opentok-web-samples repo on GitHub.

  • Publish video from a Video element. Call the captureStream() method of an HTMLVideoElement object to obtain a MediaStream object. The getVideoTracks() method of the MediaStream object returns an array of audio MediaStreamTrack objects (usually, just one). You can then use the MediaStreamTrack object as the audioSource property of the options object you pass into the OT.initPublisher() method. For a basic example, see the Publish-Video sample opentok-web-samples repo on GitHub.

You can use a video MediaStreamTrack object as the videoSource property of the options object you pass into the OT.initPublisher() method. This causes the video represented by the MediaStreamTrack object to be the video source for the published stream.

Publishing audio from audio source other than a microphone

You can set the audio source for a Publisher to an audio MediaStreamTrack object. This lets you do the following:

  • Publish audio from a Audio or Video element. Call the captureStream() method of an HTMLAudioElement object or an HTMLVideoElement object to obtain a MediaStream object. The getAudioTracks() method of the MediaStream object is an array of audio MediaStreamTrack objects (usually, just one). You can then use the MediaStreamTrack object as the audioSource property of the options object you pass into the OT.initPublisher() method.
  • Publish audio from an audio MediaStreamTrack object. For example, you can use the AudioContext object and the Web audio API to dynamically generate audio. You can then call createMediaStreamDestination().stream.getAudioTracks()[0] on the AudioContext object to get the audio MediaStreamTrack object to use as the audioSource property of the options object you pass into the OT.initPublisher() method. For a basic example, see the Stereo-Audio sample opentok-web-samples repo on GitHub.

Applying filters and effects to published audio and video

You can apply filters and effects, such as background replacement or background blur, on audio or video obtained from a microphone or camera used as the source audio or video for a published stream — see this topic.

Setting video content hints to improve video performance in certain situations

You can set a video content hint to improve the quality and performance of a published video. This can be useful in certain situations:

  • When publishing screen-sharing video that will primarily contain either text or video content.
  • When using a camera video source, if you would prefer to degrade frame rate and maintain resolution, you can set a the content hint to "text" or "detail". In a routed session, if the publisher supports using scalable video, it will send a full-resolution, low frame-rate stream and — if network conditions permit — a full-resolution, regular frame-rate stream. The OpenTok Media Router will forward one of those streams to the subscribers.

This tells the browser to use encoding or processing methods more appropriate to the type of content you specify.

Set the initial video content hint for a stream by setting the videoContentHint property of the options you pass into the OT.initPublisher() method:

var publisherOptions = {
  videoContentHint: "text",
  // other options, such as videoSource: "screen"
};
var publisher = OT.initPublisher(targetElement, publisherOptions, callbackFunction);

You can change the video content hint dynamically by calling the setVideoContentHint() method of a Publisher object:

publisher.setVideoContentHint("motion");

You can set the video content hint to one of the following values:

  • "" — No hint is provided (the default). The publishing client will make a best guess at how video content should be treated.
  • "motion" — The track should be treated as if it contains video where motion is important. For example, you may use this setting for a screen-sharing video stream that contains video.
  • "detail" — The track should be treated as if video details are extra important. For example, you may use this setting for a screen-sharing video stream that contains text content, painting, or line art.
  • "text" — The track should be treated as if text details are extra important. For example, you may use this setting for a screen-sharing video stream that contains text content.

With the "text" and "detailed" content hints, the browser attempts to maintain high resolution, even if it must reduce the video frame rate. For the "motion" content hint, the browser reduces resolution to prevent the frame rate from stalling.

You can read more about these options in the W3C Working Draft.

Chrome 60+, Safari 12.1+, Edge 79+, Opera 47+, recent versions of Samsung Internet, WebView Android 70+, and WebView on iOS 12.2+ support video content hints. The setting is ignored in other browsers.

If you can accept a slow frame rate, you may also consider restricting the frame rate of subscribed streams to improve quality.

Publisher Audio Fallback

See the developer guide for audio fallback . The publisher audio fallback feature provides enhanced bandwidth and quality monitoring to improve communications.

Other audio and video options

See the developer guide for Adjusting audio and video.

Best practices when publishing

This section includes tips for successfully publishing streams.

Allowing Device Access

It is best practice to let your users know that they are going to be asked to allow access to their camera and microphone. We find that by far the largest number of failures to publish are a result of users clicking the "deny" button or not clicking the allow button at all. We provide you with all of the events you need to be able to guide your users through this process:

publisher.on({
  accessDialogOpened: function (event) {
    // Show allow camera message
    pleaseAllowCamera.style.display = 'block';
  },
  accessDialogClosed: function (event) {
    // Hide allow camera message
    pleaseAllowCamera.style.display = 'none';
  }
});

It is also a good idea to serve your website over SSL. This is because Chrome only requires users to click to allow access to devices once per domain if that domain is served over SSL. This means that your users (if on Chrome) don't have to deal with that inconvenient allow/deny dialog box every time they load the page.

Split OT.initPublisher() and Session.publish()

Another thing we recommend is splitting the OT.initPublisher() and Session.publish() steps. This speeds up the initial connect time because you're connecting to the session while you're waiting for the user to click the allow button. So instead of:

session.connect(token, function (err) {
{... your error handling code ...}
if (!err) {
    var publisher = OT.initPublisher();
    session.publish(publisher);
  }
});

Move the OT.initPublisher() step to before you connect, as in the following:

var publisher = OT.initPublisher();
session.connect(token, function (err) {
{... your error handling code ...}
  if (!err) {
    session.publish(publisher);
  }
});

Resolution and frame rate

You can set the resolution and frame rate of the Publisher when you initialize it:

OT.initPublisher(divId, {
  resolution: '320x240',
  frameRate: 15
});

By default the resolution of a Publisher is 640x480, but you can set it to 1920x1080, 1280x720 or 320x240 as well. It is best to try to match the resolution to the size that the video will be displayed. If you are only displaying the video at 320x240 pixels then there is no point in streaming at 1280x720 or 1920x1080. Reducing the resolution can save bandwidth and reduce congestion and connection drops.

By default the frame rate of the video is 30 frames per second, but you can set it to 15, 7, or 1 as well. Reducing the frame rate can reduce the bandwidth required. Smaller resolution videos can have a lower frame rate without as much of a perceived difference to the user. So if you are using a low resolution, you might also want to think about using a low frame rate.

For more information, see the documentation for OT.initPublisher().

Troubleshooting

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

Handling Errors

There are callback methods for both Session.publish() and OT.initPublisher(). We recommend handling the error responses to both of these methods. As mentioned earlier, it is best to split up these steps and call OT.initPublisher() before you have started connecting to your Session. It also makes error handling easier if you are not calling both of these methods at the same time. This is because both error handlers will fire if there is any error publishing. It is best to wait for OT.initPublisher() to complete and Session.connect() to complete and then call Session.publish(). This way you can handle all hardware related issues in the OT.initPublisher() callback and all network related issues in the Session.publish() callback.

var connected = false,
  publisherInitialized = false;

var publisher = OT.initPublisher(function(err) {
  if (err) {
    // handle error
  } else {
    publisherInitialized = true;
    publish();
  }
});

var publish = function() {
  if (connected && publisherInitialized) {
    session.publish(publisher);
  }
};

session.connect(token, function(err) {
  if (err) {
    // handle error
  } else {
    connected = true;
    publish();
  }
});

Access Denied

The highest number of failures to OT.initPublisher() are a result of the end-user denying access to the camera and microphone. This can either be handled by listening for the accessDenied event or by listening for an error response to the OT.initPublisher() method with a code property set to 1500 and a message property set to "Publisher Access Denied:". We recommend that you handle this case and surface a message to the user indicating that they should try to publish again and allow access to the camera.

publisher.on({
  'accessDenied': function() {
    showMessage('Please allow access to the Camera and Microphone and try publishing again.');
  }
});

Device Access

Another reason for OT.initPublisher() to fail is if OpenTok cannot get access to a camera or microphone. This can happen if there is no camera or microphone attached to the machine, if there is something wrong with the driver for the camera or microphone, or if some other application is using the camera or microphone (this only happens in Windows). You can try to minimize the occurrence of these issues by using our Hardware Setup Component or by calling the OT.getDevices() method directly. However you should also handle any error when calling OT.initPublisher() because something could still go wrong. For example, the user could have denied access to the camera or microphone. In this case, the error.name property is set to "OT_USER_MEDIA_ACCESS_DENIED":

publisher = OT.initPublisher('publisher', {}, function (err) {
  if (err) {
    if (err.name === 'OT_USER_MEDIA_ACCESS_DENIED') {
      // Access denied can also be handled by the accessDenied event
      showMessage('Please allow access to the Camera and Microphone and try publishing again.');
    } else {
      showMessage('Failed to get access to your camera or microphone. Please check that your webcam'
        + ' is connected and not being used by another application and try again.');
    }
    publisher.destroy();
    publisher = null;
  }
});

Network Errors

The other reasons for failures in publishing are usually due to some kind of network failure. We handle these in the callback to Session.publish(). If the user is not connected to the network, the callback function is passed an error object with the name property set to "OT_NOT_CONNECTED". If the user is on a really restrictive network connection that does not allow for WebRTC connections, the Publisher fails to connect, and the Publisher element will just display a spinning wheel. This error has an name property set to "OT_CREATE_PEER_CONNECTION_FAILED". In this case recommend that you surface a message to the user indicating that they failed to publish and that they should check their network connection. Handling these errors looks like this:

session.publish(publisher, function(err) {
  if (err) {
    switch (err.name) {
      case "OT_NOT_CONNECTED":
        showMessage("Publishing your video failed. You are not connected to the internet.");
        break;
      case "OT_CREATE_PEER_CONNECTION_FAILED":
        showMessage("Publishing your video failed. This could be due to a restrictive firewall.");
        break;
      default:
        showMessage("An unknown error occurred while trying to publish your video. Please try again later.");
    }
    publisher.destroy();
    publisher = null;
  }
});

Losing Connectivity

Your Publisher 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. You can handle the Publisher disconnecting by listening for the streamDestroyed event with a reason property set to "networkDisconnected" like so:

publisher.on({
  streamDestroyed: function (event) {
    if (event.reason === 'networkDisconnected') {
      showMessage('Your publisher lost its connection. Please check your internet connection and try publishing again.');
    }
  }
});

Putting it all together

The following code creates a publisher, connects to a session (see Session basics), publishes a stream to the session when the client connects to the session, and detects when the publisher starts and stops streaming:

var session;
var publisher;

// Replace with the replacement element ID:
publisher = OT.initPublisher(replacementElementId);
publisher.on({
  streamCreated: function (event) {
    console.log("Publisher started streaming.");
  },
  streamDestroyed: function (event) {
    console.log("Publisher stopped streaming. Reason: "
      + event.reason);
  }
});

// Replace apiKey and sessionID with your own values:
session = OT.initSession(apiKey, sessionID);
// Replace token with your own value:
session.connect(token, function (error) {
  if (session.capabilities.publish == 1) {
    session.publish(publisher);
  } else {
    console.log("You cannot publish an audio-video stream.");
  }
});

Implementing Session Publish Retries

Transient publish failures are a known and recurring pattern in the Video API JS SDK, particularly on mobile browsers. When session.publish() 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.publish() is on the SDK roadmap. Until that ships, you need to implement this yourself.

How session.publish() Works

session.publish() can be called in two ways:

  • With a pre-initialized publisher: session.publish(publisher, callback) — you call OT.initPublisher() first, then pass the resulting publisher instance to session.publish(). This is the recommended approach as it separates media acquisition from stream creation, making error handling cleaner.
  • Without a publisher instance: session.publish(targetElement, options, callback) — the SDK internally calls OT.initPublisher() for you. In this case, both media acquisition errors and stream creation errors surface through the single session.publish() callback.

Best practice: Split OT.initPublisher() and session.publish() into separate steps. This lets you handle hardware/media errors in the OT.initPublisher() callback and network/signalling errors in the session.publish() callback — making retry logic significantly simpler and more targeted.

// Recommended: split initialization from publishing
let publisherReady = false;
let sessionConnected = false;

const publisher = OT.initPublisher('publisher-container', publisherOptions, (err) => {
  if (err) {
    handleInitPublisherError(err); // hardware/media errors — see OT.initPublisher() errors below
    return;
  }
  publisherReady = true;
  maybePublish();
});

session.connect(token, (err) => {
  if (err) { /* handle connection error */ return; }
  sessionConnected = true;
  maybePublish();
});

function maybePublish() {
  if (sessionConnected && publisherReady) {
    publishWithRetry(session, publisher);
  }
}

Why Publish Failures Happen

The most common root causes for transient publish failures are:

  • StreamCreateRequest timeouts (error 1500): The publisher failed to complete stream creation in a reasonable amount of time — typically caused by network delays during ICE/SDP negotiation.
  • mediaStopped events during the publish flow, where media device access can be interrupted.
  • Publisher object reuse without proper cleanup — reusing a publisher instance initialized with different constraints without calling unpublish and reinitializing.
  • OT_NOT_CONNECTED — attempting to publish before the session is fully connected.
  • OT_PERMISSION_DENIED — token does not have the publish role (non-retryable).

Errors from OT.initPublisher()

When you pre-initialize a publisher with OT.initPublisher(), all hardware and media acquisition errors are delivered to its completion handler — before session.publish() is ever called. Handle them in this callback using the per-error actions below: some require user action or a code fix, while transient media errors may be handled by reinitializing the publisher.

Note: If you call session.publish() without a pre-initialized publisher, these same errors will surface through the session.publish() callback instead.

error.name Description Recommended Action
OT_HARDWARE_UNAVAILABLE The hardware exists but could not be acquired (e.g. in use by another application). Prompt the user to close other applications using the device, then call OT.initPublisher() again.
OT_INVALID_PARAMETER One or more parameters passed to OT.initPublisher() were invalid. Fix the options object passed to OT.initPublisher().
OT_MEDIA_ENDED The ended event on the video element fired during initialization. Reinitialize the publisher.
OT_MEDIA_ERR_ABORTED The fetching of the stream for the video element was aborted. Reinitialize the publisher after a short delay.
OT_MEDIA_ERR_DECODE A decoding error occurred while trying to play the stream in the video element. Reinitialize the publisher after a short delay.
OT_MEDIA_ERR_NETWORK A network error caused the stream to stop being fetched. Reinitialize the publisher after a short delay.
OT_MEDIA_ERR_SRC_NOT_SUPPORTED The stream has been detected as not suitable for playback. Check the publisher's video/audio source configuration and reinitialize.
OT_NOT_SUPPORTED Something in the user media request is not supported by the browser. Inform the user and do not retry.
OT_NO_DEVICES_FOUND No audio or video input devices were found. Prompt the user to connect a device before retrying.
OT_NO_VALID_CONSTRAINTS Both video and audio were disabled — at least one must be enabled. Ensure publishAudio or publishVideo is true in the publisher options.
OT_PROXY_URL_ALREADY_SET_ERROR The proxyUrl has already been set. Setting it again will not have any effect. Set the proxy URL only once, before initializing any Session or Publisher object.
OT_REQUESTED_DEVICE_PERMISSION_DENIED The requested audio device does not have permission to be used. Prompt the user to grant device permissions.
OT_USER_MEDIA_ACCESS_DENIED The user denied access to the camera, microphone, or screen. Prompt the user to allow access in browser settings; do not retry automatically.
OT_SCREEN_SHARING_NOT_SUPPORTED Screen sharing is not supported in the current browser. Inform the user and do not retry.
OT_UNABLE_TO_CAPTURE_SCREEN Screen sharing was requested but is not supported (e.g. videoSource set to "screen", "application", or "window"). Call OT.checkScreenSharingCapability() before initializing a screen-sharing publisher.
OT_SCREEN_SHARING_EXTENSION_NOT_REGISTERED Screen-sharing requires a browser extension, but none has been registered. Register the extension before calling OT.initPublisher().
OT_SCREEN_SHARING_EXTENSION_NOT_INSTALLED Screen-sharing requires a browser extension, but it is not installed. Direct the user to install the required extension.
const publisher = OT.initPublisher('publisher-container', publisherOptions, (err) => {
  if (!err) {
    publisherReady = true;
    maybePublish();
    return;
  }

  // Hardware/media errors — handle before session.publish() is called
  switch (err.name) {
    case 'OT_REQUESTED_DEVICE_PERMISSION_DENIED':
      showMessage('Please allow access to your camera and microphone and try again.');
      break;
    case 'OT_HARDWARE_UNAVAILABLE':
    case 'OT_NO_DEVICES_FOUND':
      showMessage('Could not access your camera or microphone. Please check your devices.');
      break;
    case 'OT_SCREEN_SHARING_NOT_SUPPORTED':
    case 'OT_UNABLE_TO_CAPTURE_SCREEN':
    case 'OT_SCREEN_SHARING_EXTENSION_NOT_REGISTERED':
    case 'OT_SCREEN_SHARING_EXTENSION_NOT_INSTALLED':
      showMessage('Screen sharing is not available. Please check your browser settings.');
      break;
    default:
      showMessage('Could not initialize the publisher. Please try again.');
  }

  publisher.destroy();
});

Recoverable vs. Non-Recoverable Errors from session.publish()

Not all session.publish() errors are equal. Before implementing retry logic, it is essential to classify errors correctly — retrying on a non-recoverable error wastes time, degrades the user experience, and can mask real failures that require a different response.

Note: Error code 1500 is deprecated as a classification mechanism. Always use the error.name property to identify errors programmatically, as it maps to the specific failure scenario.

Note: When session.publish() is called without a pre-initialized publisher, media acquisition errors from OT.initPublisher() (listed above) can also surface through the session.publish() callback. In that case, treat them as non-retryable and apply the same handling described above.

Non-Recoverable Errors — Do Not Retry

These errors represent programmer mistakes, hard permission constraints, or invalid call context. Retrying will not resolve them. Instead, surface a meaningful message to the user or fix the application logic.

error.name Description Recommended Action
OT_NOT_CONNECTED session.publish() was called before the session was connected. Ensure session.connect() has completed successfully before publishing.
OT_PERMISSION_DENIED The token's role does not allow publishing (must be publisher or moderator). Inform the user they do not have publish permissions. Do not retry — generate a token with the correct role.
OT_INVALID_PARAMETER The publisher passed in is invalid, already published, or already attached to another session. Fix the application logic: call session.unpublish(publisher) before republishing, or initialize a new publisher.
OT_USER_MEDIA_ACCESS_DENIED The user denied access to the camera or microphone (or screen, for screen-sharing streams). Prompt the user to allow device access in their browser settings and try again. Do not retry automatically.
OT_CHROME_MICROPHONE_ACQUISITION_ERROR The browser failed to acquire the microphone due to a known browser bug. The end-user must restart the browser and reload the page to resolve this. Inform the user and do not retry.
OT_SCREEN_SHARING_NOT_SUPPORTED Screen sharing is not supported in the current browser. Inform the user and do not retry.
OT_SCREEN_SHARING_EXTENSION_NOT_REGISTERED Screen-sharing requires a browser extension, but none has been registered. Register the extension before attempting to publish a screen-sharing stream.
OT_SCREEN_SHARING_EXTENSION_NOT_INSTALLED Screen-sharing requires a browser extension, but it is not installed. Direct the user to install the required extension.
OT_CONSTRAINTS_NOT_SATISFIED The requested media constraints (resolution, frame rate, device) could not be satisfied by the browser. Adjust the publisher constraints and reinitialize.
OT_NO_VALID_CONSTRAINTS Both video and audio were disabled — at least one must be enabled. Ensure publishAudio or publishVideo is true before calling session.publish().
OT_NOT_SUPPORTED Something in the user media request is not supported by the browser. Inform the user and do not retry.
OT_STREAM_CREATE_FAILED The user attempted to publish in an end-to-end encryption (E2EE) enabled session without specifying an encryption key; or the stream could not be created in the server model. For E2EE sessions, ensure an encryption secret is set via session.setEncryptionSecret() before publishing.
OT_INVALID_AUDIO_OUTPUT_SOURCE An invalid audio output device ID was provided. Verify the device ID is a valid audio output device before retrying.
OT_UNABLE_TO_CAPTURE_MEDIA Unable to capture media — unknown error occurred. Inform the user and prompt them to check device availability.

Recoverable Errors — Safe to Retry

These errors are typically caused by transient network conditions, signalling timeouts, or temporary platform unavailability. They are the primary target for retry logic.

error.name Description Recommended Action
OT_TIMEOUT (code 1500) session.publish() timed out — the StreamCreateRequest did not complete in time. Most commonly caused by ICE/SDP negotiation delays or mediaStopped events. Retry with exponential backoff (up to 3 attempts). Reuse the same publisher instance if it was not destroyed.
OT_ICE_WORKFLOW_FAILED ICE negotiation failed — the peer connection could not be established. Often transient on restrictive networks. 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. Retry. If it persists, surface a message suggesting the user check their network connection.
OT_MEDIA_ERR_ABORTED / OT_MEDIA_ERR_NETWORK Media acquisition was aborted or interrupted by a network error. Retry after a short delay.
OT_MEDIA_ERR_DECODE A decoding error occurred while trying to play the stream in the video element. 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. Retry once. If it persists, check the publisher's video/audio source configuration.
OT_SET_REMOTE_DESCRIPTION_FAILED The WebRTC connection failed during setRemoteDescription. Typically a transient signalling issue. Retry with backoff. If it persists after all attempts, inform the user of a potential network issue.
OT_UNEXPECTED_SERVER_RESPONSE An unexpected error was returned from the server. Retry once after a short delay. If it persists, log the error and inform the user.

Errors Requiring a Different Action (Not a Simple Retry)

Some errors are neither a simple retry nor a hard stop — they require a specific corrective action before retrying.

error.name Description Recommended Action
OT_HARDWARE_UNAVAILABLE The camera or microphone is unavailable (e.g. in use by another application, or disconnected). Prompt the user to close other applications using the device, then reinitialize the publisher with OT.initPublisher() before retrying.
OT_NO_DEVICES_FOUND No audio or video input devices were found. Prompt the user to connect a device. Do not retry until the user confirms a device is available.
async function publishWithRetry(session, publisher, attempt = 1) {
  const MAX_RETRIES = 3;
  const RETRY_DELAY_MS = 2000;

  const error = await new Promise((resolve) => {
    session.publish(publisher, resolve);
  });

  if (!error) {
    console.log('Publishing started successfully.');
    return;
  }

  // Non-recoverable: programmer error or hard permission constraint
  const nonRetryable = [
    'OT_NOT_CONNECTED',
    'OT_PERMISSION_DENIED',
    'OT_INVALID_PARAMETER',
    'OT_USER_MEDIA_ACCESS_DENIED',
    'OT_CHROME_MICROPHONE_ACQUISITION_ERROR',
    'OT_SCREEN_SHARING_NOT_SUPPORTED',
    'OT_SCREEN_SHARING_EXTENSION_NOT_REGISTERED',
    'OT_SCREEN_SHARING_EXTENSION_NOT_INSTALLED',
    'OT_CONSTRAINTS_NOT_SATISFIED',
    'OT_NO_VALID_CONSTRAINTS',
    'OT_NOT_SUPPORTED',
    'OT_STREAM_CREATE_FAILED',
    'OT_INVALID_AUDIO_OUTPUT_SOURCE',
    'OT_UNABLE_TO_CAPTURE_MEDIA',
  ];

  // Requires corrective action before retrying
  const requiresAction = [
    'OT_HARDWARE_UNAVAILABLE',
    'OT_NO_DEVICES_FOUND',
  ];

  if (nonRetryable.includes(error.name)) {
    console.error('Non-retryable error — user action or code fix required:', error.name);
    handleNonRecoverableError(error);
    return;
  }

  if (requiresAction.includes(error.name)) {
    console.warn('Device error — prompting user before retrying:', error.name);
    handleDeviceError(error);
    return;
  }

  // Recoverable: retry with backoff
  if (attempt < MAX_RETRIES) {
    console.warn(`Publish attempt ${attempt} failed (${error.name}), retrying...`);
    await delay(RETRY_DELAY_MS * attempt);
    await publishWithRetry(session, publisher, attempt + 1);
  } else {
    console.error('All publish attempts failed. Disconnecting user.');
    handlePublishFailure(session);
  }
}

function handleNonRecoverableError(error) {
  // Surface a meaningful message to the user based on error.name
  // e.g. for OT_USER_MEDIA_ACCESS_DENIED: "Please allow camera/mic access"
}

function handleDeviceError(error) {
  // Prompt the user to check their device, then allow them to retry manually
}

function handlePublishFailure(session) {
  session.disconnect();
}

Usage:

const publisher = OT.initPublisher('publisher-container', publisherOptions);

// Wait for session to be connected before publishing
session.connect(token, (err) => {
  if (err) { /* handle connection error */ return; }
  publishWithRetry(session, publisher);
});

Important: Publisher Cleanup Before Retrying

In most failure scenarios — including OT_TIMEOUT / OT_ICE_WORKFLOW_FAILED — the publisher instance can be reused directly for the next session.publish() call. You do not need to reinitialize it.

When a publish attempt fails, the SDK tears down the stream it was attempting to create and emits a streamDestroyed event on the publisher with reason: "reset". This is expected cleanup and does not require you to reinitialize the publisher — you can retry with the same instance. Note that "reset" is a general "the publisher's stream was torn down" reason (it is also emitted when you call publisher.destroy()), so treat it as a cleanup signal rather than a dedicated publish-failure indicator; use the error.name from the session.publish() callback to decide whether to retry.

The SDK does not automatically retry session.publish() on your behalf — retry logic must be implemented at the application level as shown above.

The only case where you must reinitialize the publisher with OT.initPublisher() before retrying is when the publisher's own destroyed event fires. This event is final and indicates the publisher object itself is no longer usable.

publisher.on('destroyed', () => {
  // Publisher object is no longer usable — reinitialize before retrying
  publisher = OT.initPublisher('publisher-container', publisherOptions);
});

// A streamDestroyed event with reason 'reset' is emitted by the SDK when it tears
// down the stream (during a failed publish attempt, or when you call publisher.destroy()).
// A 'reset' during a failed publish does NOT require reinitializing the publisher.
publisher.on('streamDestroyed', (event) => {
  if (event.reason === 'reset') {
    // Expected cleanup — reuse the same publisher instance
    return;
  }
  // Handle other streamDestroyed reasons as appropriate for your application
});

What NOT to Do

  • Do not call OT.initPublisher() twice on the same publisher object with different constraints without cleaning up first (session.unpublish() → wait for streamDestroyed → then reinitialize).
  • Do not retry on OT_PERMISSION_DENIED (user denied camera/mic access) — this requires user action, not a retry.
  • Do not retry on OT_NOT_CONNECTED — ensure the session is connected before publishing.
  • Do not retry indefinitely — cap at 3 retries and gracefully handle the failure.

Handling the mediaStopped Event

The media track can be stopped mid-publish. Listen for this event and treat it as a trigger to retry:

publisher.on('mediaStopped', async () => {
  console.warn('Media stopped during publish — retrying...');
  // Unpublish if already publishing, then retry
  try { session.unpublish(publisher); } catch (e) { /* ignore */ }
  await delay(2000);
  publishWithRetry(session, publisher);
});
Parameter Recommended Value Notes
Max retries 3 Balances resilience vs. user wait time
Retry delay 2s × attempt (2s, 4s, 6s) Gives the platform time to recover
On all retries fail Disconnect user Avoids "ghost participant" state
Non-retryable errors OT_NOT_CONNECTED, OT_PERMISSION_DENIED Fail fast on these

Handling Audio Capture Issues: audioAcquisitionProblem and audioAcquisitionProblemResolved

Beyond publish-level retries, there is a separate class of audio issues that can affect an active publisher: the client's audio device may fail to deliver audio data even after a successful publish. The Video API JS SDK exposes two events specifically for this scenario.

Common Causes

The audioAcquisitionProblem event is triggered when the SDK detects — via publisher stats — that the audio track has stopped delivering bytes to the peer connection, even though getUserMedia succeeded and the publisher appears active. The most common root causes are:

  • Bluetooth audio device connected or disconnected mid-session: When a user plugs in or unplugs earphones, or connects Bluetooth headphones (e.g. AirPods) during an active session, the OS may switch the default audio device. The browser's audio pipeline can fail to re-acquire the microphone on the new device, resulting in zero audio bytes being sent.
  • Audio device change at session start: Switching audio input very early in the session — within the first 1–2 seconds of publishing — is particularly prone to triggering the issue.
  • Audio track ended by the browser or OS (trackEndedEvent): The browser can terminate the underlying audio track independently of any user action. The SDK detects this via a track.ended event and raises audioAcquisitionProblem with method: trackEndedEvent.
  • Stats-based detection (no audio bytes flowing): After the peer connection reaches the connected state, the SDK polls publisher stats roughly every few seconds. If the audio track's outbound bytesSent does not increase between consecutive polls, audioAcquisitionProblem is raised (with method: getStats). When bytesSent starts increasing again, audioAcquisitionProblemResolved is raised.

Note: The event does not always indicate a fatal failure. In some sessions, audio recovers on its own (and audioAcquisitionProblemResolved is fired); in others, the audio stream never recovers and downstream subscribers may eventually time out.

The Events

These events are emitted on the publisher instance:

  • audioAcquisitionProblem — fired when the SDK detects that the publisher has stopped sending audio (based on publisher stats), or when the underlying audio track fires an ended event. This does not necessarily mean the stream will fail, but it is an indicator that audio capture has been interrupted.
  • audioAcquisitionProblemResolved — fired when audio transmission recovers after a previous audioAcquisitionProblem. If this event fires, no corrective action is needed.

Note: These events are currently not part of the documented/typed public API (they are not declared in the SDK's TypeScript definitions). Treat them as best-effort signals that may change between versions, and verify availability against your SDK version before relying on them in production. When emitted on the publisher, they include a method property indicating how the problem was detected ('getStats' or 'trackEndedEvent').

Note: The audio acquisition checks are based on publisher stats, so there may be a short delay between the actual audio interruption and the event being raised.

Start a short timer upon receiving audioAcquisitionProblem. If audioAcquisitionProblemResolved fires before the timer expires, audio has recovered on its own and no action is needed. If the timer expires without resolution, switch the audio source as a recovery action.

publisher.on('audioAcquisitionProblem', () => {
  // Start a 3-second timer
  const timeout = setTimeout(() => {
    // Problem not resolved — attempt recovery by switching audio source
    publisher.setAudioSource(newDeviceId);
  }, 3000);

  publisher.on('audioAcquisitionProblemResolved', () => {
    // Audio recovered — clear the timer, no action needed
    clearTimeout(timeout);
  });
});

Key Considerations

  • Not a guaranteed failure indicator: audioAcquisitionProblem does not always lead to a subscription failure. Use it as an early signal to monitor and potentially act, not as a definitive failure event.
  • Monitor publisher audio stats: After receiving audioAcquisitionProblem, you can also monitor the publisher's audio stats (e.g., via publisher.getStats()) to confirm whether audio transmission has truly stopped before taking action.
  • Recovery action: Calling publisher.setAudioSource(newDeviceId) is the primary recovery mechanism. This switches the audio input device without requiring a full unpublish/republish cycle.
  • Relationship to subscription timeouts: If audio is not recovered and the publisher continues sending no audio packets, subscribers may eventually hit OT_TIMEOUT (1501). Proactive handling of audioAcquisitionProblem can help avoid this downstream failure.