feat: WebSocket real-time + rAF render gate + guest board + screen picker
Classroom performance: - WebSocket server (ws-server.js) for low-latency cursor & stroke preview Replaces HTTP POST per event → eliminates per-message auth overhead Session member cache (30s TTL) avoids SQLite query per WS message Fallback to HTTP POST when WS not connected - Cursor throttle reduced 100ms → 33ms (~30fps) - Stroke preview throttle reduced 50ms → 20ms - whiteboard.js: render() is now rAF-gated (_doRender/_rafPending) Multiple render() calls within one frame collapse into one repaint document.hidden check — zero CPU when tab is in background visibilitychange listener restores canvas on tab focus Guest board: - guestClassroom.js route: public token-based read-only access - guest-board.html: name entry + read-only whiteboard + SSE - SSE: addGuestClient/removeGuestClient/emitToGuests Screen share picker: - Discord-style modal with tab switching (screen/window/tab) - Live video preview before confirming share - useExistingScreenStream() in ClassroomRTC Fullscreen exit overlay: - #cr-fs-exit-overlay button inside cr-board-wrap - Visible only via CSS :fullscreen selector (touchpad users) File sharing from library: - Teacher picks file from library, sends as styled card in chat - crDownloadLibraryFile() fetches with Bearer auth Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -20,12 +20,14 @@
|
||||
* rtc.destroy();
|
||||
*/
|
||||
class ClassroomRTC {
|
||||
constructor({ sessionId, userId, onSignal, onScreenStream, onMicActive }) {
|
||||
constructor({ sessionId, userId, onSignal, onScreenStream, onMicActive, onMicLevel, vadThreshold }) {
|
||||
this._sid = sessionId;
|
||||
this._uid = userId;
|
||||
this._onSignal = onSignal; // fn(targetUid, payload)
|
||||
this._onScreen = onScreenStream; // fn(stream | null)
|
||||
this._onMicActive = onMicActive; // fn(uid, bool) — optional
|
||||
this._onMicLevel = onMicLevel; // fn(uid, level 0-100) — optional, fires every tick
|
||||
this._vadThreshold = vadThreshold ?? 12;
|
||||
|
||||
this._peers = new Map(); // uid → PeerState
|
||||
this._localStream = null; // mic audio
|
||||
@@ -62,15 +64,17 @@ class ClassroomRTC {
|
||||
src.connect(analyser);
|
||||
const buf = new Uint8Array(analyser.frequencyBinCount);
|
||||
let speaking = false;
|
||||
const THRESHOLD = 12;
|
||||
const THRESHOLD = this._vadThreshold;
|
||||
const timer = setInterval(() => {
|
||||
analyser.getByteFrequencyData(buf);
|
||||
const avg = buf.reduce((a, b) => a + b, 0) / buf.length;
|
||||
const level = Math.min(100, Math.round((avg / 64) * 100));
|
||||
const now = avg > THRESHOLD;
|
||||
if (now !== speaking) {
|
||||
speaking = now;
|
||||
try { this._onMicActive(uid, speaking); } catch {}
|
||||
try { if (this._onMicActive) this._onMicActive(uid, speaking); } catch {}
|
||||
}
|
||||
try { if (this._onMicLevel) this._onMicLevel(uid, level); } catch {}
|
||||
}, 120);
|
||||
this._vadTimers.set(uid, { ctx, timer });
|
||||
} catch {}
|
||||
@@ -307,10 +311,11 @@ class ClassroomRTC {
|
||||
|
||||
/* ── Screen sharing (teacher) ───────────────────────���─────────────────── */
|
||||
|
||||
async startScreenShare() {
|
||||
async startScreenShare(constraints = {}) {
|
||||
try {
|
||||
this._screenStream = await navigator.mediaDevices.getDisplayMedia({
|
||||
video: { cursor: 'always' }, audio: false,
|
||||
video: { cursor: 'always', ...constraints.video },
|
||||
audio: constraints.audio ?? false,
|
||||
});
|
||||
} catch { return null; }
|
||||
|
||||
@@ -340,6 +345,23 @@ class ClassroomRTC {
|
||||
}
|
||||
}
|
||||
|
||||
/** Take an already-acquired MediaStream (e.g. from a pre-picker) and share it. */
|
||||
async useExistingScreenStream(stream) {
|
||||
if (this._screenStream) {
|
||||
this._screenStream.getTracks().forEach(t => t.stop());
|
||||
this._screenStream = null;
|
||||
}
|
||||
this._screenStream = stream;
|
||||
const vt = this._screenStream.getVideoTracks()[0];
|
||||
if (!vt) return stream;
|
||||
for (const [, peer] of this._peers) {
|
||||
if (!peer.screenSender) {
|
||||
peer.screenSender = peer.pc.addTrack(vt, this._screenStream);
|
||||
}
|
||||
}
|
||||
return stream;
|
||||
}
|
||||
|
||||
isSharing() { return !!this._screenStream; }
|
||||
|
||||
/* ── Cleanup ─────────────────────────────────────────────────────────── */
|
||||
|
||||
Reference in New Issue
Block a user