fix: guest pre-join, post-admit lobby, audio device menu, RC keyboard (0.1.15/batch74)

1. Guest pre-join redesigned: brand backdrop, live camera preview, mic/cam toggles
   applied on entry, initials avatar, name required, "host may admit you" note.
2. Post-admit bug: the guest stayed on "Waiting for the host…" forever — the lobby
   screen had replaced the call UI and meeting-joined never re-rendered it. Now it
   rebuilds the call on admission (_inLobby).
3. Audio devices: dropped the standalone headphones button. The MIC now has a ▾ caret
   opening one Teams-style menu with Speaker + Microphone sections (radio-selected);
   speaker uses setSinkId/LiveKit switchActiveDevice, mic switches the live input.
   Mobile gets a speakerphone toggle that prefers a connected BT/headset when off.
4. Remote-control keyboard:
   - Injector now maps the PHYSICAL key (KeyboardEvent.code) instead of the character,
     so Shift+1 types "!" etc. Character mapping was why typing "performed differently".
   - Keys reach the sharer ONLY while control is ENGAGED (window focused AND you clicked
     their screen). Minimised/unfocused/chat typing stays local. Esc or clicking away
     releases; modifiers are released on disengage so nothing sticks.
   - Explicit control icons: viewer gets a Control ON/OFF button (green when engaged) +
     an on-screen hint; the SHARER gets a control icon beside mic/chat to allow/stop
     access at a glance, synced with the consent dialog and banner.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-13 13:18:01 +05:30
parent 82fa4a24d3
commit 94b8fac32f
5 changed files with 255 additions and 41 deletions
+45 -7
View File
@@ -48,6 +48,9 @@
FULL viewport underneath it. */
body.has-bar #video{width:100vw;height:100vh;}
body.has-bar{background:#0b1220;}
/* Control engaged: a green inset ring makes it obvious your keyboard now drives THEIR machine. */
#video.engaged{box-shadow:inset 0 0 0 3px #16a34a;}
#ctrlHint{position:fixed;left:50%;bottom:18px;transform:translateX(-50%);z-index:2147483000;background:rgba(15,23,42,.78);color:#fff;font-family:'Segoe UI',system-ui,sans-serif;font-size:.78rem;padding:.4rem .8rem;border-radius:999px;pointer-events:none;}
.profile{position:relative}
.profile .pbtn{display:flex;align-items:center;gap:.4rem;background:rgba(255,255,255,.14);color:#fff;border:1px solid #46598c;border-radius:10px;padding:.45rem .85rem;font-weight:600;font-size:.88rem;cursor:pointer}
.profile .pbtn:hover{background:rgba(255,255,255,.24)}
@@ -327,14 +330,22 @@ function buildBar(){
bar.style.cssText='position:fixed;right:16px;bottom:16px;z-index:2147483000;display:flex;flex-direction:row;gap:8px;align-items:center;background:rgba(15,23,42,.72);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);padding:7px 9px;border-radius:14px;box-shadow:0 8px 22px rgba(0,0,0,.35)';
const I=(n)=>(window.ic?window.ic(n,16):'');
const mic=_btn('micBtn',I('mic'),'Mic','#2563eb');
const ctrl=_btn('ctrlBtn',I('monitor'),'Control OFF — click their screen to take control','#6b7280');
const chat=_btn('chatBtn',I('chat'),'Chat','#334155');
const rec=_btn('recBtn','<svg viewBox="0 0 24 24" width="15" height="15"><circle cx="12" cy="12" r="7" fill="#ef4444"/></svg>','Record','#334155');
const end=_btn('endBtn2',I('callEnd'),'End','#dc2626');
bar.appendChild(mic);bar.appendChild(chat);bar.appendChild(rec);bar.appendChild(end);
bar.appendChild(mic);bar.appendChild(ctrl);bar.appendChild(chat);bar.appendChild(rec);bar.appendChild(end);
document.body.appendChild(bar);
document.body.classList.add('has-bar');
// Shrink from the default 48px round to a compact 38px so they read as "tiny icons".
[mic,chat,rec,end].forEach(b=>{ b.style.width='38px'; b.style.height='38px'; b.style.boxShadow='none'; });
[mic,ctrl,chat,rec,end].forEach(b=>{ b.style.width='38px'; b.style.height='38px'; b.style.boxShadow='none'; });
ctrl.onclick=()=>setEngaged(!rcEngaged);
// A hint over the screen until they take control, so it's obvious how to start driving.
if(!document.getElementById('ctrlHint')){
const h=document.createElement('div'); h.id='ctrlHint';
h.textContent='Click the screen to take control · Esc to release';
document.body.appendChild(h);
}
mic.onclick=()=>{const m=window.__mic;if(!m)return;const t=m.getAudioTracks()[0];if(!t)return;t.enabled=!t.enabled;mic.title=t.enabled?'Mute':'Unmute';mic.innerHTML='<span style="display:inline-flex">'+I(t.enabled?'mic':'micOff')+'</span>';mic.style.background=t.enabled?'#2563eb':'#6b7280';};
chat.onclick=toggleChat;
rec.onclick=()=>{ if(mediaRecorder&&mediaRecorder.state==='recording') stopRecording(); else startRecording(); };
@@ -403,12 +414,39 @@ video.addEventListener('mouseup',e=>send({kind:'mouseup',button:e.button,...rel(
video.addEventListener('dblclick',e=>send({kind:'dblclick',...rel(e)}));
video.addEventListener('wheel',e=>{e.preventDefault();send({kind:'scroll',dx:e.deltaX,dy:e.deltaY});},{passive:false});
video.addEventListener('contextmenu',e=>e.preventDefault());
// Keyboard: capture at the DOCUMENT level while a session is live so keys work regardless of which
// element has focus (the <video> lost focus as soon as you clicked the control bar → "keyboard doesn't
// work"). Skip when typing in the chat box so chat still works.
// ---- Keyboard control gating ----
// Keys reach the SHARER only while control is ENGAGED: the window is focused AND you clicked into the
// shared screen. Otherwise your typing stays on YOUR machine (so a minimised/unfocused window, or typing
// in chat, never leaks keystrokes to the remote desktop). Click the screen (or the Control button) to
// take control; click away, press Esc, or leave the window to release it.
function rcTyping(){ const a=document.activeElement; return a && (a.tagName==='INPUT'||a.tagName==='TEXTAREA'); }
document.addEventListener('keydown',e=>{ if(!video||video.style.display!=='block'||rcTyping()) return; e.preventDefault(); send({kind:'keydown',key:e.key,code:e.code}); });
document.addEventListener('keyup',e=>{ if(!video||video.style.display!=='block'||rcTyping()) return; e.preventDefault(); send({kind:'keyup',key:e.key,code:e.code}); });
let rcEngaged=false;
function setEngaged(on){
const next=!!on; if(next===rcEngaged) return;
rcEngaged=next;
if(video) video.classList.toggle('engaged', rcEngaged);
const b=document.getElementById('ctrlBtn');
if(b){ b.style.background=rcEngaged?'#16a34a':'#6b7280'; b.title=rcEngaged?'Control ON — your mouse & keyboard drive their screen (Esc to release)':'Control OFF — click their screen to take control'; }
const hint=document.getElementById('ctrlHint'); if(hint) hint.style.display=rcEngaged?'none':'block';
// Releasing control must not leave modifiers stuck down on the remote machine.
if(!rcEngaged){ ['ShiftLeft','ControlLeft','AltLeft','MetaLeft'].forEach(code=>send({kind:'keyup',key:code.replace(/Left$/,''),code})); }
}
document.addEventListener('mousedown',(e)=>{
if(!video || video.style.display!=='block') return;
if(e.target===video){ setEngaged(true); return; } // clicked the shared screen → take control
if(e.target.closest && e.target.closest('#sessionBar')) return; // control bar clicks don't release
setEngaged(false); // clicked anywhere else → release
});
window.addEventListener('blur',()=>setEngaged(false)); // window minimised / lost focus → release
document.addEventListener('keydown',e=>{
if(e.key==='Escape' && rcEngaged){ e.preventDefault(); setEngaged(false); return; }
if(!video||video.style.display!=='block'||!rcEngaged||!document.hasFocus()||rcTyping()) return;
e.preventDefault(); send({kind:'keydown',key:e.key,code:e.code});
});
document.addEventListener('keyup',e=>{
if(!video||video.style.display!=='block'||!rcEngaged||!document.hasFocus()||rcTyping()) return;
e.preventDefault(); send({kind:'keyup',key:e.key,code:e.code});
});
// Mobile viewer (#4): map touch → mouse so a phone/tablet can control too. Tap = move+click; drag = move.
const relT=(t)=>{ const c=contentRect(); return {x:Math.max(0,Math.min(1,(t.clientX-c.left)/c.width)), y:Math.max(0,Math.min(1,(t.clientY-c.top)/c.height))}; };
video.addEventListener('touchstart',e=>{ if(!e.touches.length) return; e.preventDefault(); const p=relT(e.touches[0]); send({kind:'mousemove',...p}); send({kind:'mousedown',button:0,...p}); },{passive:false});