57 lines
3.1 KiB
JavaScript
57 lines
3.1 KiB
JavaScript
// Vanguard timecode strip — browser side. Shared by the OBS browser source (/dock/clock/) and the live portal.
|
|
// Format: see bin/vgstrip.py. cell = W/160; band = round_even(cell); 64 cells from x = cell:
|
|
// [1,0,1,0] · 48-bit server ms (MSB first) · CRC-8 (poly 0x07) · [0,1,0,1]; 1 = white, 0 = black.
|
|
(function (global) {
|
|
function crc8(bytes) { let c = 0; for (const b of bytes) { c ^= b; for (let i = 0; i < 8; i++) c = (c & 0x80) ? ((c << 1) ^ 0x07) & 0xff : (c << 1) & 0xff; } return c; }
|
|
function bitsFor(ms) {
|
|
const bytes = []; let v = BigInt(ms); for (let i = 5; i >= 0; i--) { bytes[i] = Number(v & 0xffn); v >>= 8n; }
|
|
const bits = [1, 0, 1, 0];
|
|
for (const b of bytes) for (let i = 7; i >= 0; i--) bits.push((b >> i) & 1);
|
|
const c = crc8(bytes); for (let i = 7; i >= 0; i--) bits.push((c >> i) & 1);
|
|
return bits.concat([0, 1, 0, 1]);
|
|
}
|
|
function geometry(w) { const cell = w / 160; let band = Math.round(cell); band += band % 2; return { cell, band }; }
|
|
|
|
// ---- server clock offset: offset = serverNow - localNow, from the lowest-RTT of several /time samples
|
|
const sync = { offset: 0, rtt: null, ok: false, samples: 0 };
|
|
async function sampleOnce(url) {
|
|
const t0 = performance.now(), wall0 = Date.now();
|
|
const r = await fetch(url, { cache: 'no-store', credentials: 'same-origin' }); const j = await r.json();
|
|
const t1 = performance.now(); const rtt = t1 - t0;
|
|
const localMid = wall0 + rtt / 2;
|
|
return { offset: j.now_ms - localMid, rtt };
|
|
}
|
|
async function resync(url, n = 7) {
|
|
let best = null;
|
|
for (let i = 0; i < n; i++) { try { const s = await sampleOnce(url); if (!best || s.rtt < best.rtt) best = s; } catch (e) {} }
|
|
if (best) { sync.offset = best.offset; sync.rtt = best.rtt; sync.ok = true; sync.samples++; }
|
|
return sync;
|
|
}
|
|
function serverNow() { return Date.now() + sync.offset; }
|
|
|
|
// ---- draw the band into a 2D context: strip + readable clock. (w = canvas width, y0 = band top)
|
|
function drawBand(ctx, w, y0, opts = {}) {
|
|
const { cell, band } = geometry(w);
|
|
const ms = Math.round(serverNow());
|
|
ctx.fillStyle = '#000'; ctx.fillRect(0, y0, w, band);
|
|
const bits = bitsFor(ms);
|
|
const pad = Math.max(1, Math.floor(cell * 0.12));
|
|
for (let i = 0; i < bits.length; i++) {
|
|
if (!bits[i]) continue;
|
|
const x0 = Math.round(cell * (1 + i)), x1 = Math.round(cell * (2 + i));
|
|
ctx.fillStyle = '#fff'; ctx.fillRect(x0 + pad, y0 + pad, x1 - x0 - 2 * pad, band - 2 * pad);
|
|
}
|
|
if (opts.text !== false) {
|
|
const d = new Date(ms);
|
|
const tz = opts.tz || 'America/New_York';
|
|
const hms = d.toLocaleTimeString('en-US', { hour12: false, timeZone: tz });
|
|
const txt = `${hms}.${String(ms % 1000).padStart(3, '0')} ${opts.label || 'SRV'}${sync.ok ? '' : ' (unsynced)'}`;
|
|
ctx.fillStyle = sync.ok ? '#9f9' : '#f66'; ctx.font = `${Math.max(8, band - 2 * pad)}px ui-monospace, monospace`; ctx.textBaseline = 'middle';
|
|
ctx.fillText(txt, Math.round(cell * (CELLS + 2.5)), y0 + band / 2);
|
|
}
|
|
return ms;
|
|
}
|
|
const CELLS = 64;
|
|
global.VGClock = { crc8, bitsFor, geometry, resync, serverNow, drawBand, sync, CELLS };
|
|
})(window);
|