Building a video-calling feature in Agora starts with understanding how the platform handles real-time audio and video. Before writing any code, it helps to know why these pieces matter: each part you set up defines how smoothly users can see and hear one another. Video calling isn"t just about connecting two screens; it"s about creating a stable, clear, real-time link that reacts quickly to changing network conditions. With a clear view of these basics, it becomes easier to move into the implementation with purpose and confidence.
Prerequisites
- Computer and Browser: Use a computer with a modern web browser like Chrome or Firefox. Ensure your camera and microphone are working.
- Agora Account: Sign up for a free account on the Agora Console website to get an App ID and generate temporary tokens.
- Text Editor: Have a simple text editor for writing HTML and JavaScript code.
- Local Server: Install a way to run a local web server, such as Python's built-in server (run python -m http.server 8000 in the project folder) or a browser extension.
Step-by-Step Process
Set up your video call app piece by piece to build a solid foundation for real-time communication, making sure everything connects properly and works as expected.
Create a Vite Project
npm create vite@latest agora-video-call --template vanilla-ts
cd agora-video-call
npm installInstall Agora SDK
npm install agora-rtc-sdk-ng@4.24.0
npm install -D vite @types/nodeAgora Console Setup
1. agora.io/console ??? New Project ??? Get App ID
2. Generate Temp Token (channel: "test", uid: null)
3. HTTPS Required ??? Vite dev server = localhost OKUpdate index.html (index.html):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Agora Video Call v4.24.0</title>
<script type="module" src="/src/main.ts"></script>
</head>
<body>
<div id="app">
<h1>Agora Video Call (v4.24.0)</h1>
<div class="controls">
<input id="app-id" placeholder="App ID" />
<input id="channel" placeholder="Channel (test)" value="test" />
<input id="token" placeholder="Token" />
<button id="join">Join</button>
<button id="mute" disabled>Mute</button>
<button id="leave" disabled>Leave</button>
</div>
<div class="video-container">
<div id="local-video" class="video-player"></div>
<div id="remote-container"></div>
</div>
</div>
<style>
body { font-family: system-ui; margin: 0; padding: 20px; background: #f0f0f0; }
#app { max-width: 1200px; margin: 0 auto; }
.controls { background: white; padding: 20px; border-radius: 12px; margin-bottom: 20px; display: flex; gap: 10px; flex-wrap: wrap; }
.controls input { padding: 12px; border: 1px solid #ddd; border-radius: 8px; flex: 1; min-width: 200px; }
.controls button { padding: 12px 24px; border: none; border-radius: 8px; cursor: pointer; font-weight: 500; }
#join { background: #007bff; color: white; }
#mute { background: #dc3545; color: white; }
#leave { background: #6c757d; color: white; }
button:disabled { opacity: 0.5; cursor: not-allowed; }
.video-container { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
.video-player { width: 100%; height: 400px; background: #000; border-radius: 12px; }
.remote-video { width: 100%; height: 200px; background: #000; border-radius: 8px; margin-bottom: 10px; }
@media (max-width: 768px) { .video-container { grid-template-columns: 1fr; } }
</style>
</body>
</html>Complete Implementation (src/main.ts)
import * as AgoraRTC from 'agora-rtc-sdk-ng';
interface Config {
appid: string;
token: string;
channel: string;
}
const config: Partial<Config> = {
appid: '',
token: '',
channel: '',
};
let client: AgoraRTC.IRTCCreateClient;
let localAudioTrack: AgoraRTC.IMicrophoneAudioTrack;
let localVideoTrack: AgoraRTC.ICameraVideoTrack;
let remoteUsers: Record<number, AgoraRTC.IUidWithRemoteUser> = {};
async function createClient() {
client = AgoraRTC.createClient({ mode: 'rtc', codec: 'vp8' });
client.on('user-published', async (user, mediaType) => {
await client.subscribe(user, mediaType);
if (mediaType === 'video') {
const remoteVideo = document.createElement('div');
remoteVideo.id = `remote-video-${user.uid}`;
remoteVideo.className = 'remote-video';
document.getElementById('remote
container')!.appendChild(remoteVideo);
user.videoTrack!.play(remoteVideo.id);
}
if (mediaType === 'audio') {
user.audioTrack!.play();
}
});
client.on('user-unpublished', (user, mediaType) => {
if (mediaType === 'video') {
const remoteVideo = document.getElementById(`remote
video-${user.uid}`);
if (remoteVideo) remoteVideo.remove();
}
});
client.on('user-left', (user) => {
const remoteVideo = document.getElementById(`remote
video-${user.uid}`);
if (remoteVideo) remoteVideo.remove();
delete remoteUsers[user.uid];
});
}
async function join() {
if (!config.appid || !config.token || !config.channel) {
alert('Please fill all fields');
return;
}
try {
// Join channel
await client.join(config.appid, config.channel, config.token, null);
// Create tracks
[localAudioTrack, localVideoTrack] = await
AgoraRTC.createMicrophoneAndCameraTracks(
{},
{ encoderConfig: '720p_3' }
);
// Play local video
localVideoTrack.play('local-video');
// Publish tracks
await client.publish([localAudioTrack, localVideoTrack]);
// Update UI
document.getElementById('join')!.disabled = true;
document.getElementById('mute')!.disabled = false;
document.getElementById('leave')!.disabled = false;
} catch (error) {
console.error('Join failed:', error);
alert('Join failed: ' + error);
}
}
async function leave() {
try {
await client.unpublish([localAudioTrack, localVideoTrack]);
localAudioTrack.close();
localVideoTrack.close();
await client.leave();
// Reset UI
document.getElementById('local-video')!.innerHTML = '';
document.getElementById('remote-container')!.innerHTML = '';
document.getElementById('join')!.disabled = false;
document.getElementById('mute')!.disabled = true;
document.getElementById('leave')!.disabled = true;
} catch (error) {
console.error('Leave failed:', error);
}
}
async function toggleMute() {
if (localVideoTrack.isMuted) {
await localVideoTrack.setEnabled(true);
(document.getElementById('mute') as HTMLButtonElement).textContent = 'Mute';
} else {
await localVideoTrack.setEnabled(false);
(document.getElementById('mute') as HTMLButtonElement).textContent = 'Unmute';
}
}
// Event listeners
document.getElementById('app-id')!.addEventListener('input', (e) => {
config.appid = (e.target as HTMLInputElement).value;
});
document.getElementById('channel')!.addEventListener('input', (e) =>
{
config.channel = (e.target as HTMLInputElement).value;
});
document.getElementById('token')!.addEventListener('input', (e) => {
config.token = (e.target as HTMLInputElement).value;
});
document.getElementById('join')!.addEventListener('click', join);
document.getElementById('leave')!.addEventListener('click', leave);
document.getElementById('mute')!.addEventListener('click', toggleMute);
// Initialize
createClient();Run & Test
npm run dev
# Open http://localhost:5173
# Test User1: Join "test" ??? See local video
# Test User2: Same browser incognito ??? Join "test" ??? See both videosProduction Checklist
→ Vite dev server (HTTPS localhost)
→ agora-rtc-sdk-ng@4.24.0 (current)
→ TypeScript + ES modules
→ Error handling + cleanup
→ Multi-user remote video grid
→ Token auth (no uid conflicts)
→ Responsive design
Production Deployment
1. Token Server (Node.js/Agora Token Builder)
2. npm run build → Static hosting
3. HTTPS required (Vercel/Netlify)
4. Scales to 10k+ concurrent calls [web:173]

