Electron SDK Guide
Electron main/renderer split, screen capture and packaging notes.
The Electron platform is the Web SDK public surface plus a desktop capture bridge. The renderer still uses the AgoraRTC-shaped API from @nimeet/rtc-web; the desktop side additionally provides "enumerate window/screen sources" and "source-pinned screen sharing", because the browser's getDisplayMedia cannot target one specific window.
Prerequisites
- Node.js ≥ 22 and pnpm 10; Electron ≥ 28 (all three of main process, preload and renderer need changes).
contextIsolation: true(the default). This SDK's desktop bridge is injected through the preload script and does not rely onnodeIntegration.- You already have an
appIdand a backend that can issue tokens (see Enable services and get credentials).
Install
pnpm add @nimeet/sdk-electron @nimeet/rtc-webThe three entry points are used in three places: @nimeet/sdk-electron/main (main process), @nimeet/sdk-electron/preload (preload) and @nimeet/sdk-electron (renderer / shared utilities).
Get a token
Exactly as on Web: tokens are issued by your backend calling POST /v1/tokens, and the renderer only receives { appId, rtcToken, rtcUid }. The main process must not hold customerSecret either — it would show up in crash dumps and in user-readable files.
Join a channel
The main process registers the desktop bridge:
// main.js (main process)
const { app, BrowserWindow, ipcMain, desktopCapturer } = require('electron');
const { registerDesktopBridge } = require('@nimeet/sdk-electron/main');
app.whenReady().then(() => {
registerDesktopBridge({ ipcMain, desktopCapturer });
});The preload script exposes the minimal surface:
// preload.js (preload)
const { contextBridge, ipcRenderer } = require('electron');
const { exposeDesktopBridge } = require('@nimeet/sdk-electron/preload');
exposeDesktopBridge(contextBridge, ipcRenderer);In the renderer, join with the Web-shaped client (the same methods as the Web SDK):
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');
});
const uid = await client.join(appId, 'weekly-sync', rtcToken, rtcUid);Publish and subscribe
Camera and microphone work as on Web; screen sharing uses a "source" instead of the default picker:
import { pickPrimarySource, groupSources } from '@nimeet/sdk-electron';
// window.nimeetDesktop is injected by the preload script
const sources = await window.nimeetDesktop.getSources({ types: ['screen', 'window'] });
const groups = groupSources(sources); // group by "screen / window" for the dropdown
const primary = pickPrimarySource(sources); // the primary screen, used as the default
const screenTrack = await AgoraRTC.createScreenVideoTrack(
{ electronScreenSourceId: primary.id },
'disable',
);
await client.publish(screenTrack);electronScreenSourceId is the "pinned source" (the field name comes from ScreenVideoTrackInitConfig): it does not change while the user switches windows — the most important difference between the desktop platform and the browser's getDisplayMedia.
Events
The same event set as Web (user-joined / user-published / connection-state-change / token-privilege-will-expire / network-quality…), plus two extra channel events provided by the desktop bridge (prefix nimeet:desktop:):
| Event | Direction | Purpose |
|---|---|---|
nimeet:desktop:sources | renderer ← main | Returns the list of screen/window sources |
nimeet:desktop:capture-permission | renderer ← main | macOS screen-recording permission state (Chromium does not expose it) |
Error handling
There are three common error sources, and they are best handled separately:
- RTC errors: the same
AgoraRTCErroras Web (acodestring); - IPC errors: the main process did not register the bridge →
window.nimeetDesktopisundefinedin the renderer; the preload was not injected → same as above; - System permissions: when macOS "Screen Recording" is not granted, the source list is empty rather than raising an error.
const sources = await window.nimeetDesktop?.getSources({ types: ['screen'] }) ?? [];
if (sources.length === 0) {
// macOS: guide the user to System Settings → Privacy & Security → Screen Recording, tick this app and restart
}Troubleshooting
| Symptom | Cause | Handling |
|---|---|---|
window.nimeetDesktop is undefined | The preload did not run, or contextIsolation is misconfigured | Check the webPreferences.preload path and whether the file exists after packaging |
| The source list is empty (macOS) | Screen-recording permission was not granted | Grant it in System Settings and restart the app (permission changes do not apply live) |
| Sharing shows a black screen | The selected window is protected (for example a DRM player) | Switch to the whole-screen source |
The renderer reports require is not defined | CommonJS syntax was used | Write the renderer as ESM, or let the bundler handle it |
After packaging, IPC reports No handler registered | The code that registers the bridge was tree-shaken out of the main process | Call registerDesktopBridge explicitly in the main-process entry point |
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 | Electron(desktop) |
| Language | TypeScript |
| SDK package | @nimeet/sdk-electron |
| Interface version | 4.6.2 |
| Compatible Agora API | Agora electron API |
The public interfaces are aligned with the Agora electron 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 |
|---|---|---|---|
| subscribe to remote media | IBaseSpatialAudioEngine | muteRemoteAudioStream | 1 |
| Create instance / initialize | ILocalSpatialAudioEngine | initialize | 1 |
| Renew the Token | IMusicContentCenter | renewToken | 1 |
| enable video and local preview | IRtcEngine | enableVideo | 1 |
| Join a channel | IRtcEngine | joinChannel | 1 |
| Leave a channel | IRtcEngine | leaveChannel | 1 |
| Channel scenarios | IRtcEngine | setChannelProfile | 1 |
| switch roles | IRtcEngine | setClientRole | 1 |
| bind the local video | IRtcEngine | setupLocalVideo | 1 |
muteRemoteAudioStream(uid: number, mute: boolean): number
initialize(): number
renewToken(token: string): number
enableVideo(): number
joinChannel(token: string, channelId: string, uid: number, options: ChannelMediaOptions): number
leaveChannel(options?: LeaveChannelOptions): number
setChannelProfile(profile: ChannelProfileType): number
setClientRole(role: ClientRoleType, options?: ClientRoleOptions): number
setupLocalVideo(canvas: VideoCanvas): numberEvents and callbacks
Callbacks are registered on IRtcEngineEventHandler; naming differs by platform, and the full list is in the API reference for the same platform.
| Callbacks | Signature | Purpose |
|---|---|---|
onConnectionStateChanged | onConnectionStateChanged(connection: RtcConnection, state: ConnectionStateType, reason: ConnectionChangedReasonType): void | join result / connection-state changes:update the UI accordingly and decide whether to reconnect. |
onUserJoined | onUserJoined(connection: RtcConnection, remoteUid: number, elapsed: number): void | Remote user joined:allocate the frame container. |
onFirstRemoteVideoDecoded | onFirstRemoteVideoDecoded(connection: RtcConnection, remoteUid: number, width: number, height: number, elapsed: number): void | Remote video status:it can render once the first frame is decoded. |
onUserOffline | onUserOffline(connection: RtcConnection, remoteUid: number, reason: UserOfflineReasonType): void | Remote user left:release the container and subscription. |
onRequestToken | onRequestToken(connection: RtcConnection): void | Token expiring soon:call the server to renew, then `renewToken`. |
onError | onError(err: ErrorCodeType, msg: string): void | Runtime errors:`err` is an Agora error number, route by the error-code table. |
onNetworkQuality | onNetworkQuality(connection: RtcConnection, remoteUid: number, txQuality: QualityType, rxQuality: QualityType): void | Network quality:report to Analytics or prompt the user. |
Error codes
| Constant | value |
|---|---|
ErrAborted | 20 |
ErrAdmGeneralError | 1005 |
ErrAdmInitPlayout | 1008 |
ErrAdmInitRecording | 1011 |
ErrAdmStartPlayout | 1009 |
ErrAdmStartRecording | 1012 |
ErrAdmStopPlayout | 1010 |
ErrAdmStopRecording | 1013 |
ErrAlreadyInUse | 19 |
ErrBitrateLimit | 115 |
ErrBufferTooSmall | 6 |
ErrCertRaw | 1001 |
ErrClientIsBannedByServer | 123 |
ErrConnectionInterrupted | 111 |
ErrConnectionLost | 112 |
ErrDatastreamDecryptionFailed | 122 |
ErrDecryptionFailed | 120 |
ErrEncryptedStreamNotAllowedPublish | 130 |
ErrFailed | 1 |
ErrInitNetEngine | 21 |
ErrInvalidAppId | 101 |
ErrInvalidArgument | 2 |
ErrInvalidChannelName | 102 |
ErrInvalidState | 8 |
ErrInvalidToken | null |
ErrInvalidUserAccount | 134 |
ErrInvalidUserId | 121 |
ErrJoinChannelRejected | 17 |
ErrLeaveChannelRejected | 18 |
ErrNoPermission | 9 |
ErrNoServerResources | 103 |
ErrNotInChannel | 113 |
ErrNotInitialized | 7 |
ErrNotReady | 3 |
ErrNotSupported | 4 |
ErrOk | 0 |
ErrPcmsendBufferoverflow | 201 |
ErrPcmsendFormat | 200 |
ErrRefused | 5 |
ErrResourceLimited | 22 |
ErrSetClientRoleNotAuthorized | 119 |
ErrSizeTooLarge | 114 |
ErrStreamMessageTimeout | 117 |
ErrTimedout | 10 |
ErrTokenExpired | 109 |
ErrTooManyDataStreams | 116 |
ErrVdmCameraNotAuthorized | 1501 |
IRtcEngine | null |
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。