
How to Build a Desktop Video-Calling App using the Agora Electron SDK
As a JavaScript developer, you can truly use one language to build apps for almost any platform, and the Electron framework enables you to build cross-platform desktop apps. In this tutorial, we’ll walk through building a video-calling desktop app using the Agora Electron SDK for macOS and Windows.
With Electron, we can essentially bundle a website as a standalone desktop app. This approach is… sometimes controversial. But it allows a web developer to start building desktop apps in hours. So as a prerequisite you only need to know HTML, CSS, and JavaScript.
Creating an Account with Agora
Sign up at https://console.agora.io and log in to the dashboard.

Navigate to the Project List tab under the Project Management tab, and create a project by clicking the blue Create button. (When prompted to use App ID + Certificate, select only App ID.) Retrieve the App ID. It will be used to authorize your requests while you’re developing the application.
Note: This guide does not implement token authentication, which is recommended for all RTE apps running in production environments. For more information about token-based authentication in the Agora platform, see this guide: https://docs.agora.io/en/Video/token?platform=All%20Platforms.
Structure of Our Example
This is the structure of the application that we’re building:
.
├── src
│ └── index.css
│ └── index.html
│ └── index.js
│ └── render.js
├── package.json
.
Let’s Run the App
You’ll need to have the LTS version of Node.js and NPM installed:
- Make sure you have an Agora developer account, set up a project, and generate an App ID.
- Download and extract the ZIP file from the master branch.
- On macOS run
npm install
or on Windows runnpm install — arch=ia32
to install the app dependencies in the unzipped directory. - Navigate to
./src/render.js
and enter the App ID that we generated asappId = “<YourAppId>”;
- You can now run
npm start
in the project root to start your app.
That’s it. You have a video calling desktop app. The app uses "test”
as the channel name. The project uses electron-forge under the hood to get started with Electron with ease.

How the App Works
Our Electron app has four files: index.html
is the markup for our app’s elements, index.css
handles the styling for our app, render.js
contains the application logic for our video call and index.js
handles the bootstrapping process of setting up Electron.
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Agora Electron Quickstart</title>
<link rel="stylesheet" href="index.css">
<script defer src="render.js"></script>
</head>
<body>
<h1>Agora Electron Quickstart</h1>
<button id="start">Start Call</button>
<button id="stop">Stop Call</button>
<div id="video-container">
Local Feed:
<div id="local">
</div>
Remote Feed(s):
<div id="remote">
</div>
</div>
</body>
</html>
We’re adding a script tag to the <head>
tag, with the source set to render.js
so that we can load up our application logic. The defer tag waits for the page to load before executing the JS.
We have a simple layout: two buttons for starting and ending the call, and two divs to contain the videos for local and remote users inside the video-container
div.
index.css
The styling for our app looks like this:
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
margin: auto;
max-width: 90vw;
text-align: center;
align-items: center;
}
h1 {
margin-block-start: 0.4em;
margin-block-end: 0.4em;
}
button {
background-color: rgb(50, 153, 250); /* Green */
border: none;
color: white;
padding: 8px 24px;
text-align: center;
text-decoration: none;
border-radius: 4px;
font-size: 16px;
margin-bottom: 5px;
}
#local canvas {
margin: 5px 0;
height: 30vh;
}
#remote {
flex-direction: row;
display: flex;
width: 100%;
flex-wrap: wrap;
}
#remote div {
flex: 1;
margin: 5px;
}
#remote canvas {
height: 27vh;
zoom: 1.2 !important;
}
render.js
Let’s get down to business and look at our application logic:
const AgoraRtcEngine = require('agora-electron-sdk').default;
const APPID = ""; //Enter App ID here
if (APPID === "") {
alert('Please enter APPID in src/render.jsx (line:2)');
}
let rtcEngine = new AgoraRtcEngine();
rtcEngine.initialize(APPID);
rtcEngine.on('joinedChannel', (channel, uid, elapsed) => {
let localVideoContainer = document.querySelector('#local');
rtcEngine.setupLocalVideo(localVideoContainer);
})
rtcEngine.on('userJoined', (uid) => {
let remoteVideoContainer = document.querySelector('#remote')
rtcEngine.setupViewContentMode(uid, 1);
rtcEngine.subscribe(uid, remoteVideoContainer)
})
rtcEngine.setChannelProfile(0)
rtcEngine.enableVideo()
document.getElementById('start').onclick = () => {
rtcEngine.joinChannel(null, "test", null, Math.floor(new Date().getTime() / 1000))
};
document.getElementById('stop').onclick = () => {
rtcEngine.leaveChannel();
document.getElementById('local').innerHTML = '';
document.getElementById('remote').innerHTML = '';
};
We’re importing the AgoraRtcEngine
class from the SDK. We disable an alert if the user doesn’t input an Agora App ID. We create a new instance of the class — rtcEngine
. We use the App ID to initialize our engine instance.
Agora uses an events based SDK. For example when we successfully join a video channel we get the joinedChannel
event, which we can then use to execute our functions. We’re setting an event handler that calls the setupLocalVideo
method on the engine, and we pass in our div localVideoContainer
to render the local user’s video feed.
Next, we’re setting up an event for userJoined
that is triggered whenever a user joins the channel. We use thesetupViewContentMode
method to set up the remote video feed, passing in the UID from the event and 1
to use the fit mode. You can use 0
to crop the video to the size of the div. We then use the subscribe
method, which subscribes to a remote user and initializes the corresponding renderer by passing in the UID and the HTML container. We’re using the remoteVideoContainer
div.
We’re now using the setChannelProfile
method to use the communication profile. You can also use live streaming. We’re enabling the video module using the enableVideo
method.
Next, for our buttons, we’re setting onClick
functions. The start button uses the joinChannel
method to join a channel: it accepts the token, channel name, optional info, and a UID. We pass in null for the token. If you’re using an app in secure mode, you can use a temporary token. We have “test”
for our channel name, but you can use any string. Users on the same channel can communicate with each other. We pass in null for the info parameter, and we’re generating a random UID using the Date function. All users in a channel should have unique UIDs.
The stop button calls the leaveChannel
method and clears the remote videos.
index.js
Now let’s bootstrap our Electron app:
const { app, BrowserWindow } = require('electron');
const path = require('path');
app.allowRendererProcessReuse = false
if (require('electron-squirrel-startup')) {
app.quit();
}
const createWindow = () => {
const mainWindow = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
nodeIntegration: true,
contextIsolation: false
},
});
mainWindow.loadFile(path.join(__dirname, 'index.html'));
};
app.on('ready', createWindow);
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
We’re importing app
and BrowserWindow
from Electron, and we’re using path
from Node. We’re allowing the use of non-context-aware modules by setting the allowRendererProcessReuse
property to false to use the Agora SDK. The electron-squirrel-startup
module manages the Windows app startup logic.
We write a new function createWindow
to spawn a new browser window using the BrowserWindow
from Electron. We pass nodeIntegration: true
and contextIsolation: false
in our webPreferences
to support the Agora SDK integration. We load the index.html
file using the loadFile
method on our main browser window.
Returning to events on our Electron app, we have the ready event, which calls the createWindow
function. For macOS, we use the window-all-closed
event that to quit the app when the window is closed. If no windows are open we use the activate
event to create a new window using our createWindow
function.
Other Resources
That’s how easy it is to build a desktop video calling app using the Agora SDK and the Electron framework. Take a look at the Agora Electron Docs and the Agora Electron API Reference to quickly add more features like muting the camera and microphone, setting video profiles, and audio mixing.
I also invite you to join the Agora Developer Slack community. If you have any questions, you can post them in the #electron-help-me
channel.
More content at plainenglish.io