Add guest tally (on air / off air) and host connection warning

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
namandClaude Opus 5.5 committed 2026-11-06 11:35:10 -06:00
1 parent c2549bfab0
commit f5c5c593f9
7 files changed
+82 -4

No files matched your search

+13 -1
View File
@@ -53,7 +53,9 @@ function connect(){
const m = JSON.parse(ev.data);
switch (m.t) {
case 'welcome': case 'history':
log.textContent = ''; (m.history || []).forEach(x => renderMsg(log, x, 'guest')); break;
log.textContent = ''; (m.history || []).forEach(x => renderMsg(log, x, 'guest'));
if (m.t === 'welcome') setTally(!!m.onAir, false); break;
case 'onair': setTally(!!m.on, true); break;
case 'chat': renderMsg(log, m.msg, 'guest'); if (document.hidden && m.msg.who === 'host') flashTitle(); break;
case 'notice': renderMsg(log, {who:'system', text:m.text, ts:Date.now()}, 'guest'); break;
case 'answer': if (pc) await pc.setRemoteDescription({type:'answer', sdp:m.sdp}); break;
@@ -64,6 +66,16 @@ function connect(){
};
}
// ---- tally: a sign set by the host. It does not change any audio.
let tallyOn = null;
function setTally(on, announce){
if (on === tallyOn) return; tallyOn = on;
const el = $('#tally');
el.textContent = on ? '● ON AIR' : 'Off air';
el.className = 'tallybar ' + (on ? 'on' : 'off');
if (announce) note('host set the tally: ' + (on ? 'ON AIR' : 'off air'));
}
let flashT = null;
function flashTitle(){
if (flashT) return; const base = document.title; let on = false;
+32 -2
View File
@@ -16,17 +16,47 @@ function connect(){
}
let flashT = null;
function flashTitle(){ // visual only - the host page must never make sound on the rig
function flashTitle(text){ // visual only - the host page must never make sound on the rig
if (flashT) return; const base = document.title; let on = false;
flashT = setInterval(() => { document.title = (on = !on) ? '💬 Guest message' : base; }, 1000);
flashT = setInterval(() => { document.title = (on = !on) ? (text || '💬 Guest message') : base; }, 1000);
const stop = () => { clearInterval(flashT); flashT = null; document.title = base; document.removeEventListener('visibilitychange', stop); };
document.addEventListener('visibilitychange', stop);
}
// ---- tally (sign on the guest's page only)
let onAir = false;
$('#tally').onclick = () => send({t:'onair', on: !onAir});
// ---- connection warning: audible trouble on the guest -> show path over the last 10 s.
// Concealed frames (PLC) and underruns are what listeners hear; FEC recoveries are not.
let hist = [], warnLevel = 0;
function checkTrouble(c){
const box = $('#warn');
if (!c) { hist = []; box.className = 'warnbox hidden'; warnLevel = 0; return; }
const now = Date.now(), j = c.jb;
if (j) hist.push({t: now, plc: j.plc, ur: j.underruns});
while (hist.length && now - hist[0].t > 10000) hist.shift();
let plc = 0, ur = 0;
if (hist.length > 1) { const a = hist[0], b = hist[hist.length-1]; plc = b.plc - a.plc; ur = b.ur - a.ur; }
const n = (k, w) => k + ' ' + w + (k === 1 ? '' : 's');
let level = 0, msg = '';
if (c.pc === 'disconnected' || c.pc === 'failed') { level = 2; msg = 'Guest audio ' + c.pc + ' - their network dropped out.'; }
else if (c.pc === 'connected' && (plc >= 25 || ur >= 3)) { level = 2; msg = 'Guest connection is bad: ' + n(plc, 'patched gap') + ', ' + n(ur, 'dropout') + ' in the last 10 s.'; }
else if (c.pc === 'connected' && (plc >= 3 || ur >= 1)) { level = 1; msg = 'Guest connection wobbling: ' + n(plc, 'patched gap') + ', ' + n(ur, 'dropout') + ' in the last 10 s.'; }
box.textContent = msg; box.className = 'warnbox' + (level === 2 ? ' bad' : '') + (level ? '' : ' hidden');
if (level === 2 && warnLevel < 2 && document.hidden) flashTitle('⚠ Guest connection');
warnLevel = level;
}
function fmtDur(s){ const m = Math.floor(s/60); return m + ':' + String(s%60).padStart(2,'0'); }
function render(s){
$('#link').value = s.link || '';
onAir = !!s.onAir;
$('#tally').textContent = onAir ? '● ON AIR' : 'Off air';
$('#tally').classList.toggle('on', onAir);
$('#tally').disabled = !s.link;
checkTrouble(s.call);
$('#copy').disabled = $('#revoke').disabled = !s.link;
const name = s.guestName || 'Guest (anonymous)';
const c = s.call;
+6
View File
@@ -38,3 +38,9 @@ button.small{padding:3px 10px;font-size:13px}
.diag{margin-top:14px;font-size:14px}.diag summary{cursor:pointer}
.diaglog{max-height:240px;overflow:auto;background:var(--bg);border:1px solid var(--line);border-radius:7px;padding:8px;font-size:12px;white-space:pre-wrap;overflow-wrap:anywhere;margin:0}
button.on{border-color:var(--ok);color:var(--ok)}
.tallybar{margin:16px 16px 0;padding:10px 16px;border-radius:8px;text-align:center;font-weight:700;letter-spacing:.06em;border:1px solid var(--line);color:var(--muted);background:var(--card)}
.tallybar.on,button.tally.on{background:#d11a1a;border-color:#d11a1a;color:#fff}
.tallybar.on{font-size:20px;letter-spacing:.12em}
button.tally{font-weight:700;min-width:120px}
.warnbox{margin:8px 0;padding:8px 12px;border-radius:7px;border:1px solid var(--warn);background:var(--sys);font-size:14px}
.warnbox.bad{border-color:var(--danger);color:var(--danger);font-weight:600}