Spaces:
Sleeping
Sleeping
| // 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}`); | |
| }); | |