Web SDK Guide
Web SDK install, join, publish/subscribe, screen sharing and browser support.
The browser SDK exposes the same public API as Agora Web SDK 4.24.9: 142 methods, 153 overloads, 47 events and 96 error identifiers. The method chain below covers the full path from creating a client and joining a channel to publishing, subscribing and leaving, and can be reused directly in your application.
Prerequisites
- Node.js ≥ 22 (for builds and tests) and pnpm 10; run
pnpm install. - Browsers: Chrome 65+, Edge, Firefox 69+, Safari 11+ (screen sharing needs Safari 13+).
- Capturing audio and video requires a secure context:
https://orhttp://localhost; over a plainhttp://LAN IP,getUserMediafails outright. - You have obtained an
appIdas described in Enable services and get credentials, plus a backend that can issue tokens.
Install
pnpm add @nimeet/rtc-webWhen developing inside the codebase, use the workspace package: pnpm --filter @nimeet/rtc-web build. The version matches the reference version (the apiVersion in the SDK interface definition matches the interface definition section).
Get a token
The browser cannot hold customerSecret. The correct chain is your backend calling the control plane:
// Your backend (Node example):
// POST $NIMEET_API_BASE/v1/tokens { channelName, uid, role, ttlSeconds }
// The frontend only receives { appId, rtcToken, rtcUid }
const response = await fetch('/api/rtc-token?channel=weekly-sync&uid=10001');
const { appId, rtcToken, rtcUid } = await response.json();For token semantics (privilege bits, the exp/pexp split, wildcards) see AccessToken2 semantics.
Join a channel
import AgoraRTC from '@nimeet/rtc-web';
const client = AgoraRTC.createClient({ mode: 'rtc', codec: 'vp8' });
client.on('user-published', async (user, mediaType) => {
await client.subscribe(user, mediaType);
if (mediaType === 'video') user.videoTrack?.play('remote-player');
if (mediaType === 'audio') user.audioTrack?.play();
});
client.on('token-privilege-will-expire', async () => {
const fresh = await fetch('/api/rtc-token?renew=1').then((r) => r.json());
await client.renewToken(fresh.rtcToken);
});
const uid = await client.join(appId, 'weekly-sync', rtcToken, rtcUid);The page needs two containers: <div id="local-player"></div> and <div id="remote-player"></div>. The media edge address is resolved in the order surl in the token payload → window.__NIMEET_SFU_URL__ → same-origin /rtc.
Publish and subscribe
const [microphoneTrack, cameraTrack] = await AgoraRTC.createMicrophoneAndCameraTracks();
cameraTrack.play('local-player');
await client.publish([microphoneTrack, cameraTrack]);
// Manual subscription (auto-subscribe is the default; once disabled you must subscribe yourself)
await client.unsubscribe(remoteUser, 'video');
cameraTrack.setEnabled(false); // mute the video but keep publishingTracks are objects: what you publish is an ILocalTrack instance, not a "resource id". To stop publishing call client.unpublish(track); to release the capture call track.close().
Events
| Event | When it fires | Typical handling |
|---|---|---|
user-joined / user-left | A remote user enters or leaves | Allocate/release the video container |
user-published / user-unpublished | A remote user publishes or unpublishes | subscribe and then play, or stop |
connection-state-change | DISCONNECTED / CONNECTING / CONNECTED / RECONNECTING | Do not destroy tracks while RECONNECTING |
token-privilege-will-expire | 30 seconds before the privilege expires | Renew and call renewToken |
network-quality | Periodic uplink/downlink quality reports | Warn about a weak network, or report to Crystal Ball |
stream-message | Data channel messages | Whiteboard or signalling payloads |
exception | An SDK-internal exception | Collect logs and route by code |
Error handling
Every failure is an AgoraRTCError: { code, message, data? }, where code is an Agora 4.x string identifier (INVALID_PARAMS, PERMISSION_DENIED, TOKEN_EXPIRE…); the full table of 96 entries is in this platform's API reference. Platform REST error codes also carry sdkCode (a negative error number); for the mapping see SDK error identifiers per platform.
try {
await client.publish([microphoneTrack, cameraTrack]);
} catch (error) {
if (error.code === 'PERMISSION_DENIED') {
// The user denied the camera/microphone permission
} else if (error.code === 'NOT_SUPPORTED') {
// The capability is not implemented on the current media stack (for example E2EE); do not ignore it silently
}
}Troubleshooting
| Symptom | Cause | Handling |
|---|---|---|
getUserMedia throws immediately | Not a secure context, or permission denied | Use https:// or localhost; check the browser's site permissions |
Joining reports INVALID_TOKEN | The token does not match the appId/channel, or has expired | Self-check with GET /v1/tokens/verify?token=&channelName= |
| Audio works but there is no video | The remote video track was never subscribed, or the container id is wrong | Check whether the user-published handler calls play('remote-player') |
| Safari cannot capture | Capture needs a user gesture | Trigger capture from a button click handler |
Stuck in RECONNECTING | The media edge is unreachable | Check the SFU address resolution chain and the reverse proxy's WebSocket upgrade |
| Bundle size is too large | No tree-shaking | Follow Agora's on-demand import approach: import { AgoraRTC } from '@nimeet/rtc-web' |
Next steps
- Full interfaces and overloads: API reference for this platform
- Error codes and troubleshooting: Error code table, FAQ
- Issue tokens on your server: Token authentication
Platform information
| Item | value |
|---|---|
| Platform | Web(browser) |
| Language | TypeScript |
| SDK package | @nimeet/rtc-web |
| Interface version | 4.24.9 |
| Compatible Agora API | Agora javascript API |
The public interfaces are aligned with the Agora javascript API; installation and runtime requirements are listed under Prerequisites above.
Availability
This platform's SDK: Generally available. The examples use the public API; runtime capability follows the published SDK package.
join-channel flow
The method names, parameter types and return values match the API reference for the same platform. Where a method has several overloads, the most common one is shown here; see the API reference for all overloads.
| Step | Interface | Method | Overload count |
|---|---|---|---|
| Join a channel | IAgoraRTCClient | join | 1 |
| Leave a channel | IAgoraRTCClient | leave | 1 |
| publish the local track | IAgoraRTCClient | publish | 3 |
| Renew the Token | IAgoraRTCClient | renewToken | 1 |
| switch roles | IAgoraRTCClient | setClientRole | 1 |
| subscribe to remote media | IAgoraRTCClient | subscribe | 4 |
| Create instance / initialize | IAgoraRTC | createClient | 1 |
join(appid: string, channel: string, token: string|null, uid?: UID|null, options?: {autoSubscribe?:boolean;autoReceiveAndPlayAudio?:boolean;networkQualityProbe?:boolean}): Promise<UID>
leave(): Promise<void>
publish(tracks: ILocalTrack|ILocalTrack[]): Promise<void>
renewToken(token: string): Promise<void>
setClientRole(role: ClientRole, options?: ClientRoleOptions): Promise<void>
subscribe(user: IAgoraRTCRemoteUser|UID, mediaType: "video"): Promise<IRemoteVideoTrack>
createClient(config: ClientConfig): IAgoraRTCClientEvents and callbacks
| Interface | event name | listener parameters |
|---|---|---|
ILocalTrack | track-ended | () |
ILocalTrack | track-updated | track:MediaStreamTrack |
ILocalTrack | transceiver-updated | transceiver:RTCRtpTransceiver,type?:StreamType |
ILocalVideoTrack | track-ended | () |
ILocalVideoTrack | track-updated | track:MediaStreamTrack |
ILocalVideoTrack | transceiver-updated | transceiver:RTCRtpTransceiver,type?:StreamType |
ILocalVideoTrack | video-element-visible-status | data?:CheckVideoVisibleResult |
IRemoteTrack | first-frame-decoded | () |
IRemoteTrack | track-updated | track:MediaStreamTrack |
IRemoteTrack | transceiver-updated | transceiver:RTCRtpTransceiver |
IRemoteVideoTrack | first-frame-decoded | () |
IRemoteVideoTrack | sei-received | sei:Uint8Array,rtpTimestamp?:number |
IRemoteVideoTrack | track-updated | track:MediaStreamTrack |
IRemoteVideoTrack | transceiver-updated | transceiver:RTCRtpTransceiver |
IRemoteVideoTrack | video-element-visible-status | data?:CheckVideoVisibleResult |
IRemoteVideoTrack | video-state-changed | videoState:VideoState |
IAgoraRTCClient | channel-media-relay-event | event:ChannelMediaRelayEvent |
IAgoraRTCClient | channel-media-relay-state | state:ChannelMediaRelayState,code:ChannelMediaRelayError |
IAgoraRTCClient | connection-state-change | curState:ConnectionState,revState:ConnectionState,reason?:ConnectionDisconnectedReason |
IAgoraRTCClient | crypt-error | () |
IAgoraRTCClient | exception | event:{code:number;msg:string;uid:UID} |
IAgoraRTCClient | is-using-cloud-proxy | isUsingProxy:boolean |
IAgoraRTCClient | join-fallback-to-proxy | proxyServer:string |
IAgoraRTCClient | live-streaming-error | url:string,err:AgoraRTCError |
IAgoraRTCClient | live-streaming-warning | url:string,warning:AgoraRTCError |
IAgoraRTCClient | media-reconnect-end | uid:UID |
IAgoraRTCClient | media-reconnect-start | uid:UID |
IAgoraRTCClient | network-quality | stats:NetworkQuality |
IAgoraRTCClient | peerconnection-state-change | curState:RTCPeerConnectionState,revState:RTCPeerConnectionState |
IAgoraRTCClient | published-user-list | users:IAgoraRTCRemoteUser[] |
IAgoraRTCClient | stream-fallback | uid:UID,isFallbackOrRecover:"fallback"|"recover" |
IAgoraRTCClient | stream-message | uid:UID,payload:Uint8Array |
IAgoraRTCClient | stream-type-changed | uid:UID,streamType:RemoteStreamType |
IAgoraRTCClient | token-privilege-did-expire | () |
IAgoraRTCClient | token-privilege-will-expire | () |
IAgoraRTCClient | user-info-updated | uid:UID,msg:|"mute-audio"|"mute-video"|"enable-local-video"|"unmute-audio"|"unmute-video"|"disable-local-video" |
IAgoraRTCClient | user-joined | user:IAgoraRTCRemoteUser |
IAgoraRTCClient | user-left | user:IAgoraRTCRemoteUser,reason:string |
IAgoraRTCClient | user-published | user:IAgoraRTCRemoteUser,mediaType:"audio"|"video"|"datachannel",config?:IDataChannelConfig |
IAgoraRTCClient | user-unpublished | user:IAgoraRTCRemoteUser,mediaType:"audio"|"video"|"datachannel",config?:IDataChannelConfig |
IAgoraRTCClient | volume-indicator | result:{level:number;uid:UID}[] |
IAgoraRTC | audio-context-state-changed | currState:AudioContextState|"interrupted",prevState:AudioContextState|"interrupted"|undefined |
IAgoraRTC | autoplay-failed | () |
IAgoraRTC | camera-changed | deviceInfo:DeviceInfo |
IAgoraRTC | microphone-changed | deviceInfo:DeviceInfo |
IAgoraRTC | playback-device-changed | deviceInfo:DeviceInfo |
IAgoraRTC | security-policy-violation | () |
Error codes
| Constant | value |
|---|---|
ADD_CANDIDATE_FAILED | ADD_CANDIDATE_FAILED |
API_INVOKE_TIMEOUT | API_INVOKE_TIMEOUT |
BEAUTY_PROCESSOR_INTERNAL_ERROR | BEAUTY_PROCESSOR_INTERNAL_ERROR |
CAN_NOT_GET_GATEWAY_SERVER | CAN_NOT_GET_GATEWAY_SERVER |
CAN_NOT_GET_PROXY_SERVER | CAN_NOT_GET_PROXY_SERVER |
CAN_NOT_PUBLISH_MULTIPLE_VIDEO_TRACKS | CAN_NOT_PUBLISH_MULTIPLE_VIDEO_TRACKS |
CHROME_PLUGIN_NOT_INSTALL | CHROME_PLUGIN_NOT_INSTALL |
CHROME_PLUGIN_NO_RESPONSE | CHROME_PLUGIN_NO_RESPONSE |
CONSTRAINT_NOT_SATISFIED | CONSTRAINT_NOT_SATISFIED |
CONVERTING_IMAGEDATA_TO_BLOB_FAILED | CONVERTING_IMAGEDATA_TO_BLOB_FAILED |
CONVERTING_VIDEO_FRAME_TO_BLOB_FAILED | CONVERTING_VIDEO_FRAME_TO_BLOB_FAILED |
CREATE_DATACHANNEL_ERROR | CREATE_DATACHANNEL_ERROR |
CREATE_OFFER_FAILED | CREATE_OFFER_FAILED |
CROSS_CHANNEL_FAILED_JOIN_DEST | CROSS_CHANNEL_FAILED_JOIN_DEST |
CROSS_CHANNEL_FAILED_JOIN_SRC | CROSS_CHANNEL_FAILED_JOIN_SEC |
CROSS_CHANNEL_FAILED_PACKET_SENT_TO_DEST | CROSS_CHANNEL_FAILED_PACKET_SENT_TO_DEST |
CROSS_CHANNEL_SERVER_ERROR_RESPONSE | CROSS_CHANNEL_SERVER_ERROR_RESPONSE |
CROSS_CHANNEL_WAIT_STATUS_ERROR | CROSS_CHANNEL_WAIT_STATUS_ERROR |
CUSTOM_REPORT_FREQUENCY_TOO_HIGH | CUSTOM_REPORT_FREQUENCY_TOO_HIGH |
CUSTOM_REPORT_SEND_FAILED | CUSTOM_REPORT_SEND_FAILED |
DATACHANNEL_CONNECTION_TIMEOUT | DATACHANNEL_CONNECTION_TIMEOUT |
DATACHANNEL_FAILED | DATACHANNEL_FAILED |
DECODE_AUDIO_FILE_FAILED | DECODE_AUDIO_FILE_FAILED |
DEVICE_NOT_FOUND | DEVICE_NOT_FOUND |
DISCONNECT_P2P | DISCONNECT_P2P |
ELECTRON_DESKTOP_CAPTURER_GET_SOURCES_ERROR | ELECTRON_DESKTOP_CAPTURER_GET_SOURCES_ERROR |
ELECTRON_IS_NULL | ELECTRON_IS_NULL |
ENUMERATE_DEVICES_FAILED | ENUMERATE_DEVICES_FAILED |
EXCHANGE_SDP_FAILED | EXCHANGE_SDP_FAILED |
EXIST_DISABLED_VIDEO_TRACK | EXIST_DISABLED_VIDEO_TRACK |
EXTERNAL_SIGNAL_ABORT | EXTERNAL_SIGNAL_ABORT |
FETCH_AUDIO_FILE_FAILED | FETCH_AUDIO_FILE_FAILED |
GATEWAY_P2P_LOST | GATEWAY_P2P_LOST |
GET_LOCAL_CAPABILITIES_FAILED | GET_LOCAL_CAPABILITIES_FAILED |
GET_LOCAL_CONNECTION_PARAMS_FAILED | GET_LOCAL_CONNECTION_PARAMS_FAILED |
GET_VIDEO_ELEMENT_VISIBLE_ERROR | GET_VIDEO_ELEMENT_VISIBLE_ERROR |
ICE_FAILED | ICE_FAILED |
IMAGE_MODERATION_UPLOAD_FAILED | IMAGE_MODERATION_UPLOAD_FAILED |
INIT_DATACHANNEL_TIMEOUT | INIT_DATACHANNEL_TIMEOUT |
INVALID_LOCAL_TRACK | INVALID_LOCAL_TRACK |
INVALID_OPERATION | INVALID_OPERATION |
INVALID_PARAMS | INVALID_PARAMS |
INVALID_PLUGIN | INVALID_PLUGIN |
INVALID_REMOTE_USER | INVALID_REMOTE_USER |
INVALID_TRACK | INVALID_TRACK |
INVALID_UINT_UID_FROM_STRING_UID | INVALID_UINT_UID_FROM_STRING_UID |
LIVE_STREAMING_CDN_ERROR | LIVE_STREAMING_CDN_ERROR |
LIVE_STREAMING_INTERNAL_SERVER_ERROR | LIVE_STREAMING_INTERNAL_SERVER_ERROR |
LIVE_STREAMING_INVALID_ARGUMENT | LIVE_STREAMING_INVALID_ARGUMENT |
LIVE_STREAMING_INVALID_RAW_STREAM | LIVE_STREAMING_INVALID_RAW_STREAM |
LIVE_STREAMING_PUBLISH_STREAM_NOT_AUTHORIZED | LIVE_STREAMING_PUBLISH_STREAM_NOT_AUTHORIZED |
LIVE_STREAMING_TASK_CONFLICT | LIVE_STREAMING_TASK_CONFLICT |
LIVE_STREAMING_TRANSCODING_NOT_SUPPORTED | LIVE_STREAMING_TRANSCODING_NOT_SUPPORTED |
LIVE_STREAMING_WARN_FAILED_LOAD_IMAGE | LIVE_STREAMING_WARN_FAILED_LOAD_IMAGE |
LIVE_STREAMING_WARN_FREQUENT_REQUEST | LIVE_STREAMING_WARN_FREQUENT_REQUEST |
LIVE_STREAMING_WARN_STREAM_NUM_REACH_LIMIT | LIVE_STREAMING_WARN_STREAM_NUM_REACH_LIMIT |
LOCAL_AEC_ERROR | LOCAL_AEC_ERROR |
LOW_STREAM_ENCODING_ERROR | LOW_STREAM_ENCODING_ERROR |
MEDIA_OPTION_INVALID | MEDIA_OPTION_INVALID |
METADATA_OUT_OF_RANGE | METADATA_OUT_OF_RANGE |
MULTI_UNILBS_RESPONSE_ERROR | MULTI_UNILBS_RESPONSE_ERROR |
NETWORK_ERROR | NETWORK_ERROR |
NETWORK_RESPONSE_ERROR | NETWORK_RESPONSE_ERROR |
NETWORK_TIMEOUT | NETWORK_TIMEOUT |
NOT_READABLE | NOT_READABLE |
NOT_SUPPORTED | NOT_SUPPORTED |
NO_ICE_CANDIDATE | NO_ICE_CANDIDATE |
OPERATION_ABORTED | OPERATION_ABORTED |
P2P_MESSAGE_FAILED | P2P_MESSAGE_FAILED |
PB_ERROR | PB_ERROR |
PC_CLOSED | PC_CLOSED |
PERMISSION_DENIED | PERMISSION_DENIED |
PROHIBITED_OPERATION | PROHIBITED_OPERATION |
READ_LOCAL_AUDIO_FILE_ERROR | READ_LOCAL_AUDIO_FILE_ERROR |
REMOTE_USER_IS_NOT_PUBLISHED | REMOTE_USER_IS_NOT_PUBLISHED |
SENDER_NOT_FOUND | SENDER_NOT_FOUND |
SENDER_REPLACE_FAILED | SENDER_REPLACE_FAILED |
SET_ANSWER_FAILED | SET_ANSWER_FAILED |
SET_ENCODING_PARAMETER_ERROR | SET_ENCODING_PARAMETER_ERROR |
SHARE_AUDIO_NOT_ALLOWED | SHARE_AUDIO_NOT_ALLOWED |
SUBSCRIBE_FAILED | SUBSCRIBE_FAILED |
TIMEOUT | TIMEOUT |
TOKEN_EXPIRE | TOKEN_EXPIRE |
TRACK_IS_DISABLED | TRACK_IS_DISABLED |
TRACK_STATE_UNREACHABLE | TRACK_STATE_UNREACHABLE |
UID_CONFLICT | UID_CONFLICT |
UNEXPECTED_ERROR | UNEXPECTED_ERROR |
UNEXPECTED_RESPONSE | UNEXPECTED_RESPONSE |
UNSUBSCRIBE_FAILED | UNSUBSCRIBE_FAILED |
UPDATE_TICKET_FAILED | UPDATE_TICKET_FAILED |
VOID_GATEWAY_ADDRESS | VOID_GATEWAY_ADDRESS |
WEBGL_INTERNAL_ERROR | WEBGL_INTERNAL_ERROR |
WEB_SECURITY_RESTRICT | WEB_SECURITY_RESTRICT |
WS_ABORT | WS_ABORT |
WS_DISCONNECT | WS_DISCONNECT |
WS_ERR | WS_ERR |
the platform REST layer uses REST error codes;the SDK layer maps connection/authentication failures to Agora error numbers, for the mapping table see SDK error identifiers per platform。