A secure WebRTC video calling application designed for LangDAO's tutoring platform, enabling real-time video communication between students and tutors with integrated billing, session management, and professional UI matching modern video calling standards.
This WebRTC implementation facilitates high-quality video calls while providing real-time event tracking for billing, session management, and analytics integration with the LangDAO ecosystem. The service features a professional interface with session chat, language information display, and comprehensive status sharing between participants.
- 🎥 Professional Video Interface: Modern UI with role badges and status indicators
- 💬 Real-time Session Chat: Built-in messaging with timestamp synchronization
- 🌍 Multi-language Support: ISO 639 language codes with 16 supported languages
- 📊 Session Information Display: Language cards and session metadata
- 🔄 Cross-user Status Sync: Real-time mute/video status sharing between participants
- ⏱️ Continuous Session Timer: Accurate timing from session start to end
- 📱 Responsive Design: Works on desktop, tablet, and mobile devices
- 🔒 Secure WebRTC: End-to-end communication with HTTPS security
- 💓 Heartbeat System: Real-time session monitoring every 10 seconds
- 🎛️ Intuitive Controls: Clean SVG icons with visual feedback
-
Install Dependencies
npm install
-
Start Server
node server.js
-
Access Application
http://localhost:3000?room=test123&role=student&address=0x123&language=es&sessionStart=1698234567890 http://localhost:3000?room=test123&role=tutor&address=0x456&language=es&sessionStart=1698234567890
Live URL: https://langdao-production.up.railway.app
Example URLs:
- Student:
https://langdao-production.up.railway.app?room=session-abc123&role=student&address=0x742d35Cc&language=es&sessionStart=1698234567890 - Tutor:
https://langdao-production.up.railway.app?room=session-abc123&role=tutor&address=0x8ba1f109&language=es&sessionStart=1698234567890
| Parameter | Description | Values | Required | Example |
|---|---|---|---|---|
room |
Session identifier | Any string | Yes | session-abc123 |
role |
User type | student, tutor |
Yes | student |
address |
Wallet address | Ethereum address | Yes | 0x742d35Cc6634C0532925a3b8D52c0b98db8d2aD1 |
language |
Language code | ISO 639 codes | Yes | es, fr, de, ja |
sessionStart |
Session start time | Unix timestamp (ms) | Yes | 1698234567890 |
| Code | Language | Native Name |
|---|---|---|
es |
Spanish | Español |
fr |
French | Français |
de |
German | Deutsch |
it |
Italian | Italiano |
pt |
Portuguese | Português |
ru |
Russian | Русский |
ja |
Japanese | 日本語 |
ko |
Korean | 한국어 |
zh |
Chinese | 中文 |
ar |
Arabic | العربية |
nl |
Dutch | Nederlands |
sv |
Swedish | Svenska |
no |
Norwegian | Norsk |
fi |
Finnish | Suomi |
pl |
Polish | Polski |
en |
English | English |
WebRTC requires HTTPS for camera/microphone access:
- ✅ Development:
http://localhost(browser exception) - ✅ Production:
https://langdao-production.up.railway.app - ❌ HTTP in production: Browser blocks media access
The application automatically detects and uses secure protocols:
// Auto-detection logic
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
const ws = new WebSocket(`${protocol}//${location.host}?room=${roomId}`);- Local:
ws://localhost:3000 - Production:
wss://langdao-production.up.railway.app
The application sends real-time events for billing and session management:
- Session Started - Begin billing
- Timer Update - Real-time cost calculation (every second)
- User Disconnected - Stop billing immediately
- Session Ended - Final payment processing
- User Connected - Session tracking
- User Actions - Behavior logging (mute, video toggle)
Required Endpoint:
POST https://your-backend.com/api/webrtc-events
Content-Type: application/json
Environment Variable:
BACKEND_URL=https://your-backend.com/api/webrtc-eventsHeartbeat Payload (sent every 10 seconds):
{
"type": "session-heartbeat",
"sessionId": "session-abc123",
"userRole": "student",
"userAddress": "0x742d35Cc6634C0532925a3b8D52c0b98db8d2aD1",
"language": "es",
"startTime": 1698234567890,
"heartbeat": true,
"timestamp": 1698234897890,
"isAudioEnabled": true,
"isVideoEnabled": false,
"elapsedSeconds": 330
}Complete Event Types Sent to Backend:
- user-connected - When user joins the session
{
"type": "user-connected",
"sessionId": "session-abc123",
"userRole": "student",
"timestamp": 1698234567890
}- session-heartbeat - Every 10 seconds during active session
{
"type": "session-heartbeat",
"sessionId": "session-abc123",
"userRole": "student",
"userAddress": "0x742d35Cc6634C0532925a3b8D52c0b98db8d2aD1",
"language": "es",
"startTime": 1698234567890,
"heartbeat": true,
"timestamp": 1698234897890,
"isAudioEnabled": true,
"isVideoEnabled": false,
"elapsedSeconds": 330
}- user-disconnected - When user leaves or connection drops
{
"type": "user-disconnected",
"sessionId": "session-abc123",
"userRole": "student",
"reason": "connection-closed",
"timestamp": 1698234897890
}- session-ended - When call is explicitly ended
{
"type": "session-ended",
"sessionId": "session-abc123",
"totalSeconds": 330,
"endedBy": "student",
"userAddress": "0x742d35Cc6634C0532925a3b8D52c0b98db8d2aD1",
"timestamp": 1698234897890
}URL Generation for Backend:
The backend can generate WebRTC session URLs using this pattern:
// Backend URL generation example
function generateWebRTCUrl(sessionData) {
const baseUrl = 'https://langdao-production.up.railway.app';
const params = new URLSearchParams({
room: sessionData.sessionId, // e.g., "session-abc123"
role: sessionData.userRole, // "student" or "tutor"
address: sessionData.walletAddress, // Ethereum wallet address
language: sessionData.languageCode, // ISO 639 code: "es", "fr", etc.
sessionStart: Date.now() // Current timestamp
});
return `${baseUrl}?${params.toString()}`;
}
// Example usage
const studentUrl = generateWebRTCUrl({
sessionId: 'session-abc123',
userRole: 'student',
walletAddress: '0x742d35Cc6634C0532925a3b8D52c0b98db8d2aD1',
languageCode: 'es'
});
const tutorUrl = generateWebRTCUrl({
sessionId: 'session-abc123', // Same session ID
userRole: 'tutor',
walletAddress: '0x8ba1f109dDaA4bd101b53C61935e956fC7a665DE',
languageCode: 'es'
});PostMessage Listener for Parent Application:
window.addEventListener('message', (event) => {
const { type, sessionId, elapsedSeconds } = event.data;
switch(type) {
case 'session-started':
startBillingDisplay();
break;
case 'timer-update':
updateCostDisplay(elapsedSeconds);
break;
case 'user-disconnected':
handlePartnerDisconnected();
break;
case 'session-ended':
showFinalBill();
break;
}
});webRTC/
├── server.js # WebSocket signaling server
├── public/
│ ├── index.html # WebRTC client application
│ └── assets/ # UI icons
│ ├── icons8-microphone-48.png
│ ├── icons8-camera-24.png
│ └── icons8-call-button-24.png
├── package.json
├── INTEGRATION_REPORT.md # Detailed integration guide
└── README.md
- Signaling Server: Node.js WebSocket server handles offer/answer exchange
- STUN Server: Google's STUN server for NAT traversal
- Peer Connection: Direct browser-to-browser media streaming
- Event Tracking: Real-time session monitoring
// Room-based connections
const rooms = new Map(); // Track active sessions
ws.roomId = roomId; // Associate connection with room
ws.userRole = userRole; // Track user role (student/tutor)- Start: When both peers connect
- Update: Every second via timer
- Stop: On disconnection or explicit end
- Calculate:
(elapsedSeconds / 3600) * hourlyRate
| Button | Active State | Inactive State | Function |
|---|---|---|---|
| Microphone | Green + MIC icon | Red + MIC icon | Audio toggle |
| Camera | Green + CAM icon | Red + CAM icon | Video toggle |
| End Call | Red + CALL icon | - | Terminate session |
- Speaking Indicator: Green border animation on active audio
- Connection Status: Real-time display of connection state
- Timer Display: Live session duration
// Comprehensive error handling
if (!window.isSecureContext && location.protocol !== 'https:') {
throw new Error('HTTPS is required for camera/microphone access');
}
// Specific error messages
NotAllowedError: "Permission denied"
NotFoundError: "No camera/microphone found"
NotSupportedError: "Browser not supported"- WebSocket reconnection: Automatic retry on disconnect
- ICE candidate handling: Graceful failure handling
- Backend notification: Failed API calls logged but non-blocking
- Connect Repository: Link GitHub repo to Railway
- Auto-deploy: Pushes trigger automatic deployment
- Environment Variables: Set
BACKEND_URLin Railway dashboard - Domain: Use provided Railway domain or custom domain
# Using Railway CLI
railway login
railway link
railway deploy# Terminal 1
node server.js
# Terminal 2 - Open two browser tabs
open http://localhost:3000?room=test&role=student
open http://localhost:3000?room=test&role=tutor- HTTPS Access: Ensure using
https://URL - Media Permissions: Allow camera/microphone access
- Console Check: No WebSocket or media errors
- Cross-browser: Test Chrome, Firefox, Safari
- ✅ Chrome 70+
- ✅ Firefox 65+
- ✅ Safari 14+
- ✅ Edge 80+
- ❌ Internet Explorer (not supported)
Media Access Denied
- Solution: Use HTTPS, allow permissions, refresh page
WebSocket Connection Failed
- Local: Check server is running on port 3000
- Production: Verify HTTPS and wss:// protocol
Black Icon Squares
- Solution: Ensure assets copied to
public/assets/
No Video/Audio
- Check camera/microphone hardware
- Verify browser permissions
- Test with different browser
Enable console logging:
console.log('WebRTC Debug Mode');
pc.oniceconnectionstatechange = () => {
console.log('ICE state:', pc.iceConnectionState);
};- Fork the repository
- Create feature branch:
git checkout -b feature-name - Commit changes:
git commit -m 'Add feature' - Push to branch:
git push origin feature-name - Submit pull request
MIT License - see LICENSE file for details
For technical support or integration questions:
- Check
INTEGRATION_REPORT.mdfor detailed integration guide - Review browser console for error messages
- Verify HTTPS requirements are met
Built for LangDAO - Enabling seamless tutor-student video communication with integrated billing and session management.
✅ Professional Video Interface: Modern UI matching language learning platform standards ✅ Real-time Session Chat: Built-in messaging with timestamps between participants ✅ Multi-language Support: 16 supported languages with ISO 639 codes ✅ Cross-user Status Sync: Real-time mute/video status sharing ✅ Continuous Session Timer: Accurate timing that doesn't reset on peer connections ✅ Heartbeat System: 10-second interval monitoring for backend integration ✅ Lightweight Architecture: Pure HTML/CSS/JS for fast Railway deployment ✅ Secure WebRTC: HTTPS-ready with automatic protocol detection ✅ Responsive Design: Works across desktop, tablet, and mobile devices ✅ Professional Controls: Clean SVG icons with visual feedback
| Task | Status | Priority | Description |
|---|---|---|---|
| ☐ | Endpoint Setup | HIGH | Create POST /api/webrtc-events endpoint |
| ☐ | Event Handling | HIGH | Handle 4 event types: user-connected, session-heartbeat, user-disconnected, session-ended |
| ☐ | Environment Config | HIGH | Set BACKEND_URL environment variable in Railway |
| ☐ | URL Generation | HIGH | Implement session URL generation with role-based logic |
| ☐ | Billing Integration | HIGH | Start/stop billing based on heartbeat events |
| ☐ | Heartbeat Monitoring | MEDIUM | Implement disconnect detection (>25 seconds) |
| ☐ | Grace Period System | MEDIUM | Handle connection warnings and user decisions |
| ☐ | Frontend Messaging | MEDIUM | Send real-time updates to frontend via PostMessage |
| ☐ | Edge Case Handling | LOW | Handle multiple disconnects, simultaneous issues |
1. Core Endpoint Setup:
// Required endpoint implementation
app.post('/api/webrtc-events', (req, res) => {
const { type, sessionId, userRole, timestamp, elapsedSeconds } = req.body;
switch(type) {
case 'user-connected':
handleUserConnected(sessionId, userRole, timestamp);
break;
case 'session-heartbeat':
handleHeartbeat(sessionId, userRole, timestamp, elapsedSeconds, req.body);
break;
case 'user-disconnected':
handleUserDisconnected(sessionId, userRole, timestamp);
break;
case 'session-ended':
handleSessionEnded(sessionId, req.body);
break;
}
res.json({ success: true });
});2. URL Generation with Business Logic:
// Role-based URL generation
function generateWebRTCUrl(sessionData, initiatorRole) {
const baseUrl = process.env.WEBRTC_URL || 'https://langdao-production.up.railway.app';
// Business rule: Only students can initiate calls
if (initiatorRole === 'student') {
return {
studentUrl: `${baseUrl}?room=${sessionData.sessionId}&role=student&address=${sessionData.studentAddress}&language=${sessionData.languageCode}&sessionStart=${Date.now()}`,
tutorUrl: null // Generate later when tutor accepts
};
}
// Generate tutor URL only after student joins or tutor accepts
if (initiatorRole === 'tutor' && sessionData.studentConnected) {
return {
tutorUrl: `${baseUrl}?room=${sessionData.sessionId}&role=tutor&address=${sessionData.tutorAddress}&language=${sessionData.languageCode}&sessionStart=${sessionData.originalStartTime}`
};
}
throw new Error(`Invalid role or session state for URL generation`);
}3. Billing Integration:
// Billing state management
const activeSessions = new Map();
function handleHeartbeat(sessionId, userRole, timestamp, elapsedSeconds, fullData) {
const sessionKey = sessionId;
if (!activeSessions.has(sessionKey)) {
// First heartbeat - start billing
activeSessions.set(sessionKey, {
startTime: timestamp,
lastBillingUpdate: timestamp,
totalSeconds: 0,
isActive: true
});
// Notify billing system
startBilling(sessionId, fullData);
}
// Update billing every heartbeat (10 seconds)
const session = activeSessions.get(sessionKey);
session.totalSeconds = elapsedSeconds;
session.lastBillingUpdate = timestamp;
// Calculate current cost and update billing
const currentCost = (elapsedSeconds / 3600) * getHourlyRate(sessionId);
updateBilling(sessionId, currentCost, elapsedSeconds);
}
function handleUserDisconnected(sessionId, userRole, timestamp) {
// Stop billing immediately
const session = activeSessions.get(sessionId);
if (session) {
session.isActive = false;
stopBilling(sessionId, session.totalSeconds);
}
}4. Heartbeat Monitoring & Disconnect Detection:
// Connection monitoring system
const sessionHeartbeats = new Map();
function handleHeartbeat(sessionId, userRole, timestamp, elapsedSeconds, fullData) {
// Track individual user heartbeats
const userKey = `${sessionId}-${userRole}`;
sessionHeartbeats.set(userKey, {
lastSeen: timestamp,
sessionId,
userRole,
elapsedSeconds
});
// ... billing logic above
}
// Monitor for disconnections every 15 seconds
setInterval(() => {
const now = Date.now();
const DISCONNECT_THRESHOLD = 25000; // 25 seconds (2.5x heartbeat interval)
sessionHeartbeats.forEach((data, userKey) => {
if (now - data.lastSeen > DISCONNECT_THRESHOLD) {
console.log(`User ${data.userRole} disconnected from session ${data.sessionId}`);
// Send disconnect warning to frontend
sendDisconnectWarning(data.sessionId, data.userRole);
// Remove to avoid spam
sessionHeartbeats.delete(userKey);
}
});
}, 15000);5. Grace Period System:
// Grace period management
const gracePeriods = new Map();
function sendDisconnectWarning(sessionId, disconnectedRole) {
const gracePeriodSeconds = 30; // 30 second grace period
// Store grace period state
gracePeriods.set(sessionId, {
disconnectedRole,
startTime: Date.now(),
duration: gracePeriodSeconds * 1000,
resolved: false
});
// Send to frontend via your messaging system
sendToFrontend(sessionId, {
type: 'connection-warning',
disconnectedRole,
gracePeriodSeconds,
sessionId
});
// Auto-end session if no decision made
setTimeout(() => {
const grace = gracePeriods.get(sessionId);
if (grace && !grace.resolved) {
handleGracePeriodExpired(sessionId);
}
}, gracePeriodSeconds * 1000);
}
// Handle grace period decisions from frontend
app.post('/api/grace-period-decision', (req, res) => {
const { sessionId, decision, decidedBy } = req.body; // decision: 'wait' or 'end'
const grace = gracePeriods.get(sessionId);
if (grace) {
grace.resolved = true;
if (decision === 'end') {
// End session immediately
endSession(sessionId, decidedBy);
} else {
// Continue waiting - reset disconnect detection
console.log(`Grace period granted for session ${sessionId}`);
}
gracePeriods.delete(sessionId);
}
res.json({ success: true });
});Q: What happens when someone disconnects for >25 seconds?
A: The backend should detect this via missing heartbeats and send a connection-warning to the frontend, prompting the remaining user to either wait 30 seconds or end the session immediately. This prevents billing for dead connections while giving users a chance to reconnect.
Q: Who controls call initiation - students vs tutors? A: This is a business logic decision. The backend should implement role-based URL generation. For example: only students can initiate calls, tutors get URLs only after students connect. Implement this in your URL generation function, not in the WebRTC app.
Q: When should billing start and stop?
A: Start billing on the first session-heartbeat (both users connected), update every 10 seconds, stop immediately on user-disconnected or session-ended. Never bill during grace periods or when only one user is connected.
1. Integration Method:
- Embed WebRTC interface in iframe or popup window
- Implement PostMessage listener to receive session events
- Handle billing display updates in real-time
2. User Flow:
- Backend generates session URLs and sends to both participants
- Users click URLs to join video call
- Parent application receives events for billing updates
- Session ends when either user clicks "Exit" or closes window
3. Event Handling:
session-started: Begin billing displaytimer-update: Update cost display every seconduser-disconnected: Show partner disconnected statesession-ended: Process final payment and show session summary
- Production URL:
https://langdao-production.up.railway.app - HTTPS Required: Camera/microphone access blocked on HTTP in production
- Environment Variables: Set
BACKEND_URLin Railway dashboard - Auto-deployment: Enabled on git push to connected repository