Publish: Diagnostics
This guide covers how to gather diagnostics for publishers and resolve common issues.
Getting statistics about a publisher's stream
The Vonage Video SDK exposes detailed stream-quality metrics through a high-level statistics API—recommended for most use cases—which provides audio, video, network, and sender-side statistics in a unified, session-aware form that remains stable across peer-connection transitions. For advanced debugging, the SDK also offers access to the raw WebRTC stats report, which reflects unprocessed peer-connection data.
Refer to the client observability developer guide for detailed information.
Test 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 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.
You can use the SubscriberKit.setAudioStatsListener(AudioStatsListener listener) and SubscriberKit.setVideoStatsListener(VideoStatsListener listener) methods of the Subscriber object to get audio and video statistics for the stream you publish.
See this topic for more information.
You can use the networkStatsDelegate method of the OTSubscriberKit object to get audio and video statistics for the stream you publish.
The vonage-video-api-network-test-samples repo includes sample code for showing how to use statistics of a test stream before publishing to a session.
You can use the networkStatsDelegate method of the OTSubscriberKit object to get audio and video statistics for the stream you publish.
The vonage-video-api-network-test-samples repo includes sample code for showing how to use statistics of a test stream before publishing to a session.
You can then subscribe to the stream and use the Subscriber.AudioStatsUpdated and Subscriber.VideoStatsUpdated events to get audio and video statistics for the stream you publish.
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.
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 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.');
}
}
});
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.
Why Publish Failures Happen
The most common root causes for transient publish failures are:
- StreamCreateRequest timeouts (error 1500):
OT.Publisherfailed to publish in a reasonable amount of time — typically caused by media stop events or network delays during ICE/SDP negotiation. mediaStoppedevents 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
unpublishand reinitializing. OT_NOT_CONNECTED— attempting to publish before the session is fully connected.OT_USER_MEDIA_ACCESS_DENIED— device access issues (non-retryable).
Recoverable vs. Non-Recoverable Errors
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.
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. | Prompt the user to allow device access in their browser settings and try again. Do not retry automatically. |
OT_CHROME_MICROPHONE_ACQUISITION_ERROR |
Chrome-specific: the microphone could not be acquired (e.g. hardware issue or OS-level block). | Destroy the publisher and prompt the user to check their microphone. This is not recoverable through retrying. |
OT_SCREEN_SHARING_NOT_SUPPORTED |
Screen sharing is not supported in the current browser. | Inform the user and do not retry. |
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. |
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. |
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. |
Putting It Together: Recommended Retry Pattern with Error Classification
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_CONSTRAINTS_NOT_SATISFIED',
];
// 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
If the publisher was destroyed between attempts (e.g., you received a streamDestroyed or destroyed event on the publisher), you must reinitialize it with OT.initPublisher() before calling session.publish() again. Reusing a destroyed publisher object will not work.
publisher.on('destroyed', () => {
// Publisher was destroyed — reinitialize before retrying
publisher = OT.initPublisher('publisher-container', publisherOptions);
});
If the publisher was not destroyed (the session.publish() callback returned an error but the publisher object is still alive), you can retry with the same publisher instance.
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 forstreamDestroyed→ 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);
});
Summary of Recommended Parameters
| 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 atrack.endedevent and raisesaudioAcquisitionProblemwithmethod: trackEndedEvent. - Stats-based detection (no audio bytes flowing): The SDK continuously monitors publisher stats after the peer connection is established. If no audio bytes are detected within 30 seconds of the connection reaching the connected state,
audioAcquisitionProblemis raised withmethod: getStats.
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). 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 previousaudioAcquisitionProblem. If this event fires, no corrective action is needed.
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.
Recommended Recovery Pattern
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:
audioAcquisitionProblemdoes 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., viapublisher.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 ofaudioAcquisitionProblemcan help avoid this downstream failure.