krishgokul92 commited on
Commit
af7d7e4
·
verified ·
1 Parent(s): e907a38

Update public/index.html

Browse files
Files changed (1) hide show
  1. public/index.html +139 -85
public/index.html CHANGED
@@ -140,10 +140,10 @@
140
  // --- Elements ---
141
  const adminView = document.getElementById('adminView');
142
  const clientView = document.getElementById('clientView');
143
- const roomBadge = document.getElementById('roomBadge');
144
  const stateBadge = document.getElementById('stateBadge');
145
  const blackoutEl = document.getElementById('blackout');
146
- const timeEl = document.getElementById('time');
147
 
148
  // Show correct view
149
  if (role === 'admin') {
@@ -154,7 +154,10 @@
154
  stateBadge.classList.remove('hidden');
155
  }
156
 
157
- // --- Socket.IO connection (rooms + role passed as query) ---
 
 
 
158
  const socket = io({ query: { role, room } });
159
 
160
  // Ask server to refresh stats on admin load
@@ -162,96 +165,82 @@
162
 
163
  // --- Wake lock (client) ---
164
  let wakeLock;
165
- async function keepAwake() {
166
- try { if ('wakeLock' in navigator) wakeLock = await navigator.wakeLock.request('screen'); }
167
- catch(_) {}
168
- }
169
  if (role === 'client') {
170
  keepAwake();
171
- document.addEventListener('visibilitychange', () => {
172
- if (document.visibilityState === 'visible') keepAwake();
173
- });
174
  }
175
 
176
- // --- Admin: server clock via NTP-style sync ---
177
- const roomNameEl = document.getElementById('roomName');
178
- const statsEl = document.getElementById('stats');
179
- const serverNowEl = document.getElementById('serverNow');
180
- if (roomNameEl) roomNameEl.textContent = room;
 
181
 
182
  function syncOnce() { socket.emit('sync:ping', { t0: Date.now() }); }
183
- if (role === 'admin') {
184
- setInterval(syncOnce, 1000);
185
- syncOnce();
186
- }
187
- socket.on('sync:pong', ({ t0, t1, t2 }) => {
188
- if (role !== 'admin') return;
189
- const t3 = Date.now();
190
- const offset = ((t1 - t0) + (t2 - t3)) / 2;
191
- serverNowEl.textContent = String((Date.now() + offset)|0);
192
- });
193
 
194
- socket.on('stats', ({ numAdmins, numClients }) => {
195
- if (statsEl) statsEl.textContent = `Clients: ${numClients} • Admins: ${numAdmins}`;
196
- });
197
-
198
- // --- Admin buttons ---
199
- const delayEl = document.getElementById('delay');
200
- const labelEl = document.getElementById('label');
201
- const startBtn = document.getElementById('start');
202
- const stopBtn = document.getElementById('stop');
203
- const resetBtn = document.getElementById('reset');
204
- const blackOnBtn = document.getElementById('blackOn');
205
- const blackOffBtn = document.getElementById('blackOff');
 
206
 
207
- if (startBtn) startBtn.onclick = () => {
208
- socket.emit('admin:start', {
209
- delayMs: Number(delayEl.value || 3000),
210
- label: labelEl.value || ''
211
- });
212
- };
213
- if (stopBtn) stopBtn.onclick = () => socket.emit('admin:stop');
214
- if (resetBtn) resetBtn.onclick = () => socket.emit('admin:reset');
215
- if (blackOnBtn) blackOnBtn.onclick = () => socket.emit('admin:blackout', { on: true });
216
- if (blackOffBtn) blackOffBtn.onclick = () => socket.emit('admin:blackout', { on: false });
217
 
218
- // --- Client clock sync (best-of samples) ---
219
- const offsets = []; // {delay, offset}
220
- function clientSyncOnce() { socket.emit('sync:ping', { t0: Date.now() }); }
221
- if (role === 'client') {
222
- for (let i=0;i<10;i++) setTimeout(clientSyncOnce, i*150);
223
- setInterval(clientSyncOnce, 3000);
224
- }
225
  socket.on('sync:pong', ({ t0, t1, t2 }) => {
226
- if (role !== 'client') return;
227
  const t3 = Date.now();
228
  const delay = (t3 - t0) - (t2 - t1);
229
  const offset = ((t1 - t0) + (t2 - t3)) / 2; // server - client
230
  offsets.push({ delay, offset, ts: t3 });
231
- if (offsets.length > 40) offsets.shift();
232
  });
 
233
  function bestOffset() {
234
  if (!offsets.length) return 0;
235
- const best = [...offsets].sort((a,b)=>a.delay-b.delay).slice(0,7).map(x=>x.offset);
236
- return Math.round(best.reduce((s,v)=>s+v,0)/best.length);
 
 
237
  }
238
 
239
- // --- Client timer state machine ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
240
  const State = { IDLE:'IDLE', RUNNING:'RUNNING', STOPPED:'STOPPED' };
241
  let state = State.IDLE;
242
- let zeroPerfTs = null; // when timer hits 00:00 (in performance.now() space)
243
  let rafId = 0;
244
 
245
  function setState(s){ state=s; if (stateBadge) stateBadge.textContent = s; }
246
- function serverMsToLocalPerf(msServer) {
247
- const off = bestOffset(); // server - client
248
- const localNowWall = Date.now();
249
- const localNowPerf = performance.now();
250
- const whenLocalWall = msServer - off;
251
- const delta = whenLocalWall - localNowWall;
252
- return localNowPerf + delta;
253
- }
254
- // Fixed 5-char format "SS:CC"
255
  function fmt(ms) {
256
  if (ms < 0) ms = 0;
257
  const totalCs = Math.floor(ms / 10);
@@ -259,19 +248,43 @@
259
  const cs = totalCs % 100;
260
  return `${String(secs).padStart(2,'0')}:${String(cs).padStart(2,'0')}`;
261
  }
262
- function renderElapsed(ms){ if (timeEl) timeEl.textContent = fmt(ms); }
263
- function loop(){
264
- const now = performance.now();
265
- const ms = now - zeroPerfTs;
266
- renderElapsed(ms);
267
- rafId = requestAnimationFrame(loop);
268
  }
269
- function startAtServerTime(startAt){
270
- zeroPerfTs = serverMsToLocalPerf(startAt);
 
 
 
 
271
  setState(State.RUNNING);
 
 
272
  cancelAnimationFrame(rafId);
273
- rafId = requestAnimationFrame(loop);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
274
  }
 
275
  function stopPause(){
276
  if (state !== State.RUNNING) return;
277
  setState(State.STOPPED);
@@ -280,18 +293,41 @@
280
  function resetAll(){
281
  cancelAnimationFrame(rafId);
282
  setState(State.IDLE);
283
- renderElapsed(0);
284
  }
285
 
286
- // --- Command handling ---
 
 
287
  socket.on('cmd', (msg) => {
288
  if (!msg || !msg.type) return;
289
  switch (msg.type) {
290
- case 'start': startAtServerTime(msg.startAt); break;
291
- case 'stop': stopPause(); break;
292
- case 'reset': resetAll(); break;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
293
  case 'blackout':
294
- // Only clients should go black
295
  if (role === 'client') {
296
  blackoutEl.style.display = msg.on ? 'block' : 'none';
297
  document.documentElement.style.cursor = msg.on ? 'none' : 'auto';
@@ -304,8 +340,26 @@
304
  if (role === 'client') {
305
  document.getElementById('roomBadge').textContent = `room: ${room}`;
306
  setState(State.IDLE);
307
- renderElapsed(0);
308
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
309
  </script>
310
  </body>
311
  </html>
 
140
  // --- Elements ---
141
  const adminView = document.getElementById('adminView');
142
  const clientView = document.getElementById('clientView');
143
+ const roomBadge = document.getElementById('roomBadge');
144
  const stateBadge = document.getElementById('stateBadge');
145
  const blackoutEl = document.getElementById('blackout');
146
+ const timeEl = document.getElementById('time');
147
 
148
  // Show correct view
149
  if (role === 'admin') {
 
154
  stateBadge.classList.remove('hidden');
155
  }
156
 
157
+ // Optional: guard admin from any blackout element existing
158
+ if (role !== 'client') { const el = document.getElementById('blackout'); if (el) el.remove(); }
159
+
160
+ // --- Socket.IO connection (rooms + role) ---
161
  const socket = io({ query: { role, room } });
162
 
163
  // Ask server to refresh stats on admin load
 
165
 
166
  // --- Wake lock (client) ---
167
  let wakeLock;
168
+ async function keepAwake() { try { if ('wakeLock' in navigator) wakeLock = await navigator.wakeLock.request('screen'); } catch(_) {} }
 
 
 
169
  if (role === 'client') {
170
  keepAwake();
171
+ document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') keepAwake(); });
 
 
172
  }
173
 
174
+ // =========================
175
+ // Precise clock sync pieces
176
+ // =========================
177
+ const offsets = []; // { delay, offset, ts }
178
+ let preSyncFast = false; // enable high-rate sync until start
179
+ let preSyncUntil = 0;
180
 
181
  function syncOnce() { socket.emit('sync:ping', { t0: Date.now() }); }
 
 
 
 
 
 
 
 
 
 
182
 
183
+ // normal cadence
184
+ let syncNormal = setInterval(syncOnce, 3000);
185
+ // burst cadence (activated by 'preSync' or when start is scheduled)
186
+ let syncBurstTimer = null;
187
+ function startBurstSync() {
188
+ if (syncBurstTimer) return;
189
+ preSyncFast = true;
190
+ syncBurstTimer = setInterval(syncOnce, 50); // 20 Hz
191
+ }
192
+ function stopBurstSync() {
193
+ preSyncFast = false;
194
+ if (syncBurstTimer) { clearInterval(syncBurstTimer); syncBurstTimer = null; }
195
+ }
196
 
197
+ // do some quick initial samples
198
+ for (let i = 0; i < 10; i++) setTimeout(syncOnce, i * 120);
 
 
 
 
 
 
 
 
199
 
200
+ // ingest samples
 
 
 
 
 
 
201
  socket.on('sync:pong', ({ t0, t1, t2 }) => {
 
202
  const t3 = Date.now();
203
  const delay = (t3 - t0) - (t2 - t1);
204
  const offset = ((t1 - t0) + (t2 - t3)) / 2; // server - client
205
  offsets.push({ delay, offset, ts: t3 });
206
+ if (offsets.length > 120) offsets.shift();
207
  });
208
+
209
  function bestOffset() {
210
  if (!offsets.length) return 0;
211
+ // choose the lowest-delay decile and average
212
+ const sorted = [...offsets].sort((a,b)=>a.delay-b.delay);
213
+ const slice = sorted.slice(0, Math.max(5, Math.floor(sorted.length/10)));
214
+ return Math.round(slice.reduce((s,x)=>s+x.offset,0) / slice.length);
215
  }
216
 
217
+ // derive server time on this client (ms)
218
+ function serverNow() { return Date.now() + bestOffset(); }
219
+
220
+ // --- Admin: server clock view + stats
221
+ const roomNameEl = document.getElementById('roomName');
222
+ const statsEl = document.getElementById('stats');
223
+ const serverNowEl= document.getElementById('serverNow');
224
+ if (roomNameEl) roomNameEl.textContent = room;
225
+
226
+ if (role === 'admin') {
227
+ setInterval(() => { serverNowEl.textContent = String(serverNow()|0); }, 1000);
228
+ }
229
+ socket.on('stats', ({ numAdmins, numClients }) => {
230
+ if (statsEl) statsEl.textContent = `Clients: ${numClients} • Admins: ${numAdmins}`;
231
+ });
232
+
233
+ // =========================
234
+ // Timer state (client side)
235
+ // =========================
236
  const State = { IDLE:'IDLE', RUNNING:'RUNNING', STOPPED:'STOPPED' };
237
  let state = State.IDLE;
238
+ let startAtServerMs = 0; // the server-time moment when timer hits 00:00
239
  let rafId = 0;
240
 
241
  function setState(s){ state=s; if (stateBadge) stateBadge.textContent = s; }
242
+
243
+ // Fixed 5-char format "SS:CC" (centiseconds)
 
 
 
 
 
 
 
244
  function fmt(ms) {
245
  if (ms < 0) ms = 0;
246
  const totalCs = Math.floor(ms / 10);
 
248
  const cs = totalCs % 100;
249
  return `${String(secs).padStart(2,'0')}:${String(cs).padStart(2,'0')}`;
250
  }
251
+
252
+ // Renders from *server* clock so all clients show the same number at the same instant
253
+ function render() {
254
+ const elapsed = serverNow() - startAtServerMs;
255
+ timeEl.textContent = fmt(elapsed);
256
+ rafId = requestAnimationFrame(render);
257
  }
258
+
259
+ // Optional: final busy-wait in the last ~3 ms before T0 to reduce sub-frame wobble
260
+ const BUSY_WAIT_MS = 0; // set to 3 for ultra-tight start; 0 keeps CPU friendly
261
+
262
+ function armStartAt(startAt) {
263
+ startAtServerMs = startAt;
264
  setState(State.RUNNING);
265
+
266
+ // stop any previous loop
267
  cancelAnimationFrame(rafId);
268
+
269
+ // schedule first frame near T0
270
+ const schedule = () => {
271
+ const dt = startAtServerMs - serverNow();
272
+ if (dt <= (BUSY_WAIT_MS || 1)) {
273
+ // optional busy-wait
274
+ if (BUSY_WAIT_MS > 0) {
275
+ const end = performance.now() + BUSY_WAIT_MS;
276
+ while (performance.now() < end) {} // spin ~3ms
277
+ }
278
+ // start rendering loop
279
+ rafId = requestAnimationFrame(render);
280
+ } else {
281
+ // check again soon; use shorter checks as we approach T0
282
+ setTimeout(schedule, Math.min(50, Math.max(5, dt/10)));
283
+ }
284
+ };
285
+ schedule();
286
  }
287
+
288
  function stopPause(){
289
  if (state !== State.RUNNING) return;
290
  setState(State.STOPPED);
 
293
  function resetAll(){
294
  cancelAnimationFrame(rafId);
295
  setState(State.IDLE);
296
+ if (timeEl) timeEl.textContent = "00:00";
297
  }
298
 
299
+ // ==============
300
+ // Command intake
301
+ // ==============
302
  socket.on('cmd', (msg) => {
303
  if (!msg || !msg.type) return;
304
  switch (msg.type) {
305
+ case 'preSync':
306
+ // enter high-rate sync until start
307
+ preSyncUntil = msg.until || 0;
308
+ startBurstSync();
309
+ // automatically stop burst shortly after T0
310
+ setTimeout(() => stopBurstSync(), Math.max(1000, preSyncUntil - Date.now() + 1000));
311
+ break;
312
+
313
+ case 'start':
314
+ // If we didn't receive preSync for some reason, still start burst now
315
+ if (!preSyncFast) {
316
+ preSyncUntil = msg.startAt;
317
+ startBurstSync();
318
+ setTimeout(() => stopBurstSync(), Math.max(1000, preSyncUntil - Date.now() + 1000));
319
+ }
320
+ armStartAt(msg.startAt);
321
+ break;
322
+
323
+ case 'stop':
324
+ stopPause(); break;
325
+
326
+ case 'reset':
327
+ resetAll(); break;
328
+
329
  case 'blackout':
330
+ // Only clients go black
331
  if (role === 'client') {
332
  blackoutEl.style.display = msg.on ? 'block' : 'none';
333
  document.documentElement.style.cursor = msg.on ? 'none' : 'auto';
 
340
  if (role === 'client') {
341
  document.getElementById('roomBadge').textContent = `room: ${room}`;
342
  setState(State.IDLE);
343
+ timeEl.textContent = "00:00";
344
  }
345
+
346
+ // --- Admin controls wiring ---
347
+ const delayEl = document.getElementById('delay');
348
+ const labelEl = document.getElementById('label');
349
+ const startBtn = document.getElementById('start');
350
+ const stopBtn = document.getElementById('stop');
351
+ const resetBtn = document.getElementById('reset');
352
+ const blackOnBtn = document.getElementById('blackOn');
353
+ const blackOffBtn = document.getElementById('blackOff');
354
+
355
+ if (startBtn) startBtn.onclick = () => socket.emit('admin:start', {
356
+ delayMs: Number(delayEl.value || 3000),
357
+ label: labelEl.value || ''
358
+ });
359
+ if (stopBtn) stopBtn.onclick = () => socket.emit('admin:stop');
360
+ if (resetBtn) resetBtn.onclick = () => socket.emit('admin:reset');
361
+ if (blackOnBtn) blackOnBtn.onclick = () => socket.emit('admin:blackout', { on: true });
362
+ if (blackOffBtn) blackOffBtn.onclick = () => socket.emit('admin:blackout', { on: false });
363
  </script>
364
  </body>
365
  </html>