A real-time voice chat room built with Agora WebRTC, Firebase, and vanilla JavaScript. No accounts, no installs — open a link and talk. Create separate spaces instantly with a URL parameter.
- 🎙️ Voice calls — join/leave a persistent space with mic mute, per-user volume sliders, and speaking indicators
- 💬 Persistent chat — messages stored in Firebase. Supports images, video, audio, YouTube, Spotify, and file embeds automatically from URLs
- 🏠 Multiple spaces — each
?space=URL parameter creates a fully isolated space with its own voice channel, chat history, whiteboard, and presence. Share a link likeyoursite.com?space=gamingto invite someone into a specific space - 🎭 Session identities — every tab/device has a unique Firebase/Agora session; generated funny names are random and collision-free, while custom names may be shared
- 💤 Solo AFK disconnect — an inactive user who is alone in voice receives a warning after 15 minutes and automatically leaves Agora after 30 minutes while remaining connected to chat
- 🖥️ Screen sharing — 1080p/30fps with optional system audio capture
- 🎨 Shared whiteboard — real-time collaborative canvas with drawing tools and eraser, synced via Firebase
- 🎮 Word guessing game — drawer picks a word, others guess via chat. 60-second timer, confetti on correct guess
- 📊 Polls — create live multi-option polls with atomic vote counting
- 🤖 AI bot —
/botcommand hits Gemini API via proxy, falls back through multiple models if rate-limited - 📱 Mobile-friendly — chat auto-collapses on join, touch-optimised controls, wake lock prevents screen sleep during calls
- Push notifications — each device follows its latest visited space and receives only one alert while that space has unread messages
| Command | Description |
|---|---|
/bot <question> |
Ask the AI a question (visible to everyone) |
/poll Question , Option1 , Option2 |
Create a live poll |
/nick <name> |
Change your display name |
/roll <max> |
Roll a random number (default 1–100) |
/msg <user[#session]> <message> |
Send a private message; duplicate names are disambiguated by session ID |
/ping |
Show Agora network stats (RTT + user count) |
/space |
Change space |
/crtkica |
Open / close the whiteboard (desktop only) |
/clear |
Clear your local chat view |
/help |
Show command reference card |
?name=HairyPloper sets your display name for the session
?space=friday-night joins a specific space
?space=friday-night&name=HairyPloper both at once
Name is saved to localStorage only when it is provided through ?name= or /nick. Without an explicit name, a new random Serbian-style funny nickname is assigned on every page load; Firebase presence replaces it with another random free funny name if a collision occurs. Generated funny names therefore stay unique among active sessions. Explicit custom names are labels rather than identifiers, so multiple sessions may deliberately use the same name. Firebase presence stores a lowercase concatenated identityKey for collision checks and name lookup. Previously cached generated names are discarded automatically.
Space isolates everything — voice channel, chat, whiteboard, and presence are all scoped per space. The active value is always shown as Space: <name> in the chat header. If no ?space= is provided, the saved space is restored, falling back to the default space. Space names are case-insensitive (Gaming, GAMING, and gaming resolve to the same room) and accept letters, numbers, dashes and underscores only (a-z A-Z 0-9 - _). Serbian diacritics and spaces are stripped automatically, so stick to ASCII names.
Each tab/device claims an atomic, unique session entry in Firebase presence as soon as chat initializes, so chat-only users are represented too. Generated funny names and icons are selected randomly from currently unused values, while custom names may be duplicated: a PC and phone can both appear as Anton, with different Firebase presence keys and Agora UIDs. Chat messages store only the session ID and anonymous Firebase user ID needed for session features and own-message alignment; the push device ID remains private to the notification flow, and the space is already encoded in the Firebase path. Disconnect cleanup is armed before presence is written. On page refresh or navigation the client explicitly closes Firebase so the server executes onDisconnect; a page restored from mobile back/forward cache reconnects and reclaims presence. The presence entry stays hidden from the voice grid until voiceJoined becomes true. Leaving voice keeps the chat session, while closing or disconnecting the page releases it. When /msg encounters duplicate custom names it lists name#session choices so the sender can select exactly one recipient. The whiteboard game likewise tracks its drawer by session ID.
✅ ?space=gaming
✅ ?space=friday-night
✅ ?space=pako_and_friends
❌ ?space=petak veče (space stripped → falls back to default)
❌ ?space=cet123 (this works, diacritics must be avoided)
| Layer | Technology |
|---|---|
| Voice / video | Agora WebRTC SDK (RTC mode, VP8) |
| Realtime data | Firebase Realtime Database |
| Auth | Firebase Anonymous Auth |
| AI | Google Gemini API (via Vercel proxy) |
| File hosting | Catbox.moe / Litterbox (temporary) |
| Frontend | Vanilla JS (ES6+), HTML5 Canvas, Web Audio API |
No build step. No framework. No bundler.
voice_room_web/
├── index.html # Entry point — Firebase config lives here
├── css/
│ └── style.css
├── js/
│ ├── main.js # App init, globals, audio settings, speaker selection
│ ├── rtc.js # Agora: join/leave, mic, screen share, reconnection
│ ├── chat.js # Firebase chat, slash commands, polls, AI bot, file upload
│ ├── ui.js # User cards, video overlays, background video/music
│ ├── utils.js # Shared helpers: escapeHtml, playTone, wakeLock, sanitizer
│ └── whiteboard.js # Canvas drawing, Firebase stroke sync, word game
└── src/ # Static assets (audio, etc.)
- Firebase project with Realtime Database and Anonymous Auth enabled
- Agora account with an App ID
- Gemini API key + a proxy to forward requests (the default proxy is a Vercel function)
-
Clone
git clone <repository-url> cd voice_room_web
-
Firebase — paste your config object into
index.html:const firebaseConfig = { apiKey: "...", authDomain: "...", databaseURL: "...", projectId: "...", };
-
Agora — set your App ID in
js/main.js:window.APP_ID = "your-agora-app-id";
⚠️ For production, replace thenulltoken inclient.join()with server-generated short-lived tokens to prevent unauthorised channel access. -
AI proxy — update the fetch URL in
js/chat.jsaskAI()to point at your own Gemini proxy:fetch("https://your-proxy.vercel.app/api/gemini", ...)
-
Open
index.htmldirectly in a browser or serve with any static server:npx serve .
AEC (echo cancellation), AGC (gain control), and ANS (noise suppression) can be toggled per-session from the settings menu. Choices are saved to localStorage. Speaker output device can also be selected after joining (desktop only).
Solo voice users are considered active when they interact with the page or speak into the microphone. Another voice user joining stops the AFK countdown, and the timer starts fresh when the last other voice user leaves. After 15 inactive minutes the chat shows a warning; after 30 minutes the normal leave flow disconnects Agora but keeps chat and its identity reservation active. Adjust APP_CONFIG.afkTimeoutMs and APP_CONFIG.afkWarningMs in js/main.js to change these intervals.
For live timer diagnostics, run window.getAfkStatus() in the browser console. To watch it update every second, run window.afkWatch = setInterval(() => console.table(window.getAfkStatus()), 1000). Stop watching with clearInterval(window.afkWatch).
| Browser | Voice | Screen share | Whiteboard |
|---|---|---|---|
| Chrome 80+ | ✅ | ✅ | ✅ |
| Edge 80+ | ✅ | ✅ | ✅ |
| Firefox 75+ | ✅ | ✅ | ✅ |
| Safari 13+ | ✅ | ✅ | |
| iOS Safari | ✅ | ❌ | ❌ |
| Chrome Mobile | ✅ | ❌ | ❌ |
Open source. Use and modify freely.