live-class / server.js
mlmihjaz's picture
v3: full conference — all participants cam/mic/screen, TURN relay, group chat, host mute controls, students muted by default
7084875
Raw
History Blame Contribute Delete
7.2 kB
// Sri AI Live Class - signaling + auth server
// Multi-room conference: every participant can publish cam/mic/screen; teacher = host
const express = require('express');
const http = require('http');
const path = require('path');
const { WebSocketServer } = require('ws');
const PORT = process.env.PORT || 3010;
// ---- Dummy accounts (replace with srigen auth on migration) ----
const USERS = {
'teacher@sriai.lk': { password: 'teach123', role: 'teacher', name: 'Ms. Nova' },
'teacher2@sriai.lk': { password: 'teach123', role: 'teacher', name: 'Mr. Silva' },
'student1@sriai.lk': { password: 'stud123', role: 'student', name: 'Aisha' },
'student2@sriai.lk': { password: 'stud123', role: 'student', name: 'Kasun' },
'student3@sriai.lk': { password: 'stud123', role: 'student', name: 'Tharushi' },
};
const app = express();
app.use(express.json());
app.use(express.static(path.join(__dirname, 'public')));
app.post('/login', (req, res) => {
const { email, password } = req.body || {};
const u = USERS[(email || '').toLowerCase().trim()];
if (!u || u.password !== password) {
return res.status(401).json({ ok: false, error: 'Invalid email or password' });
}
res.json({ ok: true, role: u.role, name: u.name, email });
});
const server = http.createServer(app);
const wss = new WebSocketServer({ server });
let nextId = 1;
const clients = new Map(); // id -> { ws, role, name, roomId }
const rooms = new Map(); // roomId -> { id, name, teacherId, teacherName, chat: [] }
function send(id, msg) {
const c = clients.get(id);
if (c && c.ws.readyState === 1) c.ws.send(JSON.stringify(msg));
}
function roomMembers(roomId) {
return [...clients.entries()].filter(([, c]) => c.roomId === roomId);
}
function roomBroadcast(roomId, msg, exceptId = null) {
for (const [id] of roomMembers(roomId)) if (id !== exceptId) send(id, msg);
}
function classList() {
return [...rooms.values()].map(r => ({
id: r.id, name: r.name, teacher: r.teacherName,
students: roomMembers(r.id).filter(([, c]) => c.role === 'student').length,
}));
}
function broadcastClassList() {
const msg = { type: 'class-list', classes: classList() };
for (const [id, c] of clients) {
if (c.role === 'student' && !c.roomId) send(id, msg);
}
}
function endRoom(roomId, reason) {
const room = rooms.get(roomId);
if (!room) return;
for (const [mid, mc] of roomMembers(roomId)) {
if (mid === room.teacherId) continue;
mc.roomId = null;
send(mid, { type: 'class-ended', reason });
}
rooms.delete(roomId); // chat history dies with the room
broadcastClassList();
}
function teacherRoom(teacherId) {
return [...rooms.values()].find(r => r.teacherId === teacherId);
}
wss.on('connection', (ws) => {
const id = nextId++;
let joined = false;
ws.on('message', (raw) => {
let msg;
try { msg = JSON.parse(raw); } catch { return; }
if (msg.type === 'hello') {
clients.set(id, { ws, role: msg.role, name: msg.name, roomId: null });
joined = true;
send(id, { type: 'welcome', id });
if (msg.role === 'student') send(id, { type: 'class-list', classes: classList() });
return;
}
if (!joined) return;
const c = clients.get(id);
switch (msg.type) {
// WebRTC signaling relay (perfect-negotiation payloads)
case 'signal': {
const target = clients.get(msg.to);
if (target && target.roomId && target.roomId === c.roomId) {
send(msg.to, { type: 'signal', from: id, data: msg.data });
}
break;
}
// announce which of my stream ids is camera vs screen
case 'meta':
if (c.roomId) roomBroadcast(c.roomId, { type: 'meta', from: id, cam: msg.cam || null, screen: msg.screen || null }, id);
break;
// mic/cam/sharing status for UI icons
case 'state':
if (c.roomId) roomBroadcast(c.roomId, { type: 'state', from: id, mic: !!msg.mic, cam: !!msg.cam, sharing: !!msg.sharing }, id);
break;
case 'chat': {
if (!c.roomId) break;
const room = rooms.get(c.roomId);
const text = String(msg.text || '').slice(0, 500).trim();
if (!room || !text) break;
const entry = { from: id, name: c.name, role: c.role, text, ts: Date.now() };
room.chat.push(entry);
if (room.chat.length > 300) room.chat.shift();
roomBroadcast(c.roomId, { type: 'chat', ...entry });
break;
}
// host (teacher) mutes a student's mic or turns off their camera
case 'host-control': {
const room = teacherRoom(id);
const target = clients.get(msg.to);
if (room && target && target.roomId === room.id && ['mute-mic', 'stop-cam'].includes(msg.action)) {
send(msg.to, { type: 'host-control', action: msg.action });
}
break;
}
// ---- teacher ----
case 'create-class': {
if (c.role !== 'teacher') break;
const existing = teacherRoom(id);
if (existing) endRoom(existing.id, 'Teacher started a new class');
const roomId = 'r' + id + '-' + nextId++;
const name = String(msg.name || '').slice(0, 60).trim() || `${c.name}'s Class`;
rooms.set(roomId, { id: roomId, name, teacherId: id, teacherName: c.name, chat: [] });
c.roomId = roomId;
send(id, { type: 'class-created', roomId, name });
broadcastClassList();
break;
}
case 'end-class': {
const room = teacherRoom(id);
if (room) { endRoom(room.id, 'Class ended by teacher'); c.roomId = null; }
break;
}
// ---- student ----
case 'join-class': {
if (c.role !== 'student') break;
const room = rooms.get(msg.roomId);
if (!room) { send(id, { type: 'class-list', classes: classList() }); break; }
const participants = roomMembers(room.id).map(([mid, mc]) => ({ id: mid, name: mc.name, role: mc.role }));
c.roomId = room.id;
send(id, {
type: 'joined', roomId: room.id, className: room.name,
teacherName: room.teacherName, teacherId: room.teacherId,
participants, chat: room.chat,
});
roomBroadcast(room.id, { type: 'peer-joined', id, name: c.name, role: c.role }, id);
broadcastClassList();
break;
}
case 'leave-class': {
if (!c.roomId) break;
const roomId = c.roomId;
c.roomId = null;
roomBroadcast(roomId, { type: 'peer-left', id });
send(id, { type: 'class-list', classes: classList() });
broadcastClassList();
break;
}
case 'list-classes':
send(id, { type: 'class-list', classes: classList() });
break;
}
});
ws.on('close', () => {
if (!joined) return;
const c = clients.get(id);
clients.delete(id);
if (!c) return;
if (c.role === 'teacher') {
const room = teacherRoom(id);
if (room) endRoom(room.id, 'Teacher disconnected');
} else if (c.roomId) {
roomBroadcast(c.roomId, { type: 'peer-left', id });
broadcastClassList();
}
});
});
server.listen(PORT, () => {
console.log(`Sri AI Live Class running at http://localhost:${PORT}`);
});