fix(rc+guest): coordinate/keyboard/UI for remote control; guest media + panel fixes (batch71)

Remote control (connect.html viewer):
- Coordinate offset fixed: map clicks to the actual video CONTENT rect (object-fit
  letterbox-aware), not the element rect — cursor now lands where you click.
- Keyboard: capture at document level while a session is live (the <video> lost
  focus on bar clicks → keys did nothing). Skips the chat input.
- Control bar moved to bottom-right with tiny modern icons; video fills the viewport.
- Smoother cursor (mousemove ~60/s).

Guest meetings:
- Guest mic inaudible + guest invisible in the participant list + outsider screen
  share not showing (#1/#8/#11): root cause was the SFU media→tile map keyed on
  identity==uid, but guests had a random LiveKit identity and a null signaling uid.
  Guests now carry ONE stable id across signaling (meeting-join guestId) and the
  LiveKit token identity, so their media attaches and they appear to everyone.
- Guests can't add participants (#9) and don't see the transcript button (#12).
- Search box in the schedule participant list (#10).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-11 14:28:37 +05:30
parent e562099344
commit bd488b3286
4 changed files with 60 additions and 26 deletions
+36 -15
View File
@@ -43,10 +43,10 @@
.topbar2{background:var(--card);border-bottom:1px solid var(--line);padding:.5rem 1rem;display:none;justify-content:space-between;align-items:center;}
.topbar2.show{display:flex;} #barStatus{font-weight:600;font-size:.9rem;color:var(--blue);}
#endBtn{padding:.45rem 1rem;background:#fee2e2;color:#b91c1c;border:none;border-radius:8px;font-weight:600;cursor:pointer;}
#video{width:100vw;height:calc(100vh - 46px);background:#0b1220;object-fit:contain;display:none;cursor:crosshair;outline:none;}
/* Control bar sits vertically on the RIGHT; reserve a thin right strip so the screen uses the full
height/width otherwise (maximised view) and the icons never overlay the shared content. */
body.has-bar #video{width:calc(100vw - 76px);}
#video{width:100vw;height:100vh;background:#0b1220;object-fit:contain;display:none;cursor:crosshair;outline:none;}
/* The control bar floats over the bottom-right corner (tiny icons), so the shared screen uses the
FULL viewport underneath it. */
body.has-bar #video{width:100vw;height:100vh;}
body.has-bar{background:#0b1220;}
.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}
@@ -323,15 +323,19 @@ function buildBar(){
{ const hl=document.getElementById('homeLink'); if(hl) hl.style.display='none'; }
bzcSession(true);
const bar=document.createElement('div'); bar.id='sessionBar';
bar.style.cssText='position:fixed;right:14px;top:50%;transform:translateY(-50%);z-index:2147483000;display:flex;flex-direction:column;gap:10px;align-items:center;background:rgba(15,23,42,.94);padding:12px 8px;border-radius:16px;box-shadow:0 10px 28px rgba(0,0,0,.35)';
const mic=_btn('micBtn',SVG_MIC,'Mic','#2563eb');
const chat=_btn('chatBtn',SVG_CHAT,'Chat','#475569');
const rec=_btn('recBtn',SVG_REC,'','#0ea5e9'); rec.title='Record'; rec.querySelectorAll('span').forEach((s,i)=>{ if(i>0) s.remove(); });
const end=_btn('endBtn2',SVG_END,'End','#dc2626');
// Floats over the bottom-right corner with tiny icons, so the shared screen fills the whole viewport.
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 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);
document.body.appendChild(bar);
document.body.classList.add('has-bar'); // reserve space so the bar never overlays the shared screen
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">'+(t.enabled?SVG_MIC:SVG_MICOFF)+'</span>';mic.style.background=t.enabled?'#2563eb':'#6b7280';};
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.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(); };
end.onclick=()=>{ try{ws.send(JSON.stringify({type:'end-session',sessionId,reason:'agent-ended'}));}catch(_){} };
@@ -378,16 +382,33 @@ async function setupPeer(){
else if(s==='connected'){ clearTimeout(pc._dt); } };
}
const send=(o)=>{if(inputChannel&&inputChannel.readyState==='open')inputChannel.send(JSON.stringify(o));};
const rel=(e)=>{const r=video.getBoundingClientRect();return{x:(e.clientX-r.left)/r.width,y:(e.clientY-r.top)/r.height};};
// Map a pointer event to NORMALIZED coords over the actual VIDEO CONTENT, not the <video> element.
// object-fit:contain letterboxes the shared screen inside the element (bars top/bottom or sides), so
// mapping against the element rect put the cursor off by the bar size (the "must aim lower" bug). We
// compute the content rect from the source resolution (videoWidth/Height) and map against that.
function contentRect(){
const r=video.getBoundingClientRect();
const vw=video.videoWidth||16, vh=video.videoHeight||9;
const elAR=r.width/r.height, vidAR=vw/vh;
let cw, ch, ox=0, oy=0;
if(vidAR>elAR){ cw=r.width; ch=r.width/vidAR; oy=(r.height-ch)/2; } // letterbox: bars top & bottom
else { ch=r.height; cw=r.height*vidAR; ox=(r.width-cw)/2; } // pillarbox: bars left & right
return {left:r.left+ox, top:r.top+oy, width:cw, height:ch};
}
const rel=(e)=>{ const c=contentRect(); let x=(e.clientX-c.left)/c.width, y=(e.clientY-c.top)/c.height; return {x:Math.max(0,Math.min(1,x)), y:Math.max(0,Math.min(1,y))}; };
let lm=0;
video.addEventListener('mousemove',e=>{const t=performance.now();if(t-lm<30)return;lm=t;send({kind:'mousemove',...rel(e)});});
video.addEventListener('mousemove',e=>{const t=performance.now();if(t-lm<16)return;lm=t;send({kind:'mousemove',...rel(e)});}); // ~60/s for a smoother cursor
video.addEventListener('mousedown',e=>{video.focus();send({kind:'mousedown',button:e.button,...rel(e)});});
video.addEventListener('mouseup',e=>send({kind:'mouseup',button:e.button,...rel(e)}));
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());
video.addEventListener('keydown',e=>{e.preventDefault();send({kind:'keydown',key:e.key,code:e.code});});
video.addEventListener('keyup',e=>{e.preventDefault();send({kind:'keyup',key:e.key,code:e.code});});
// 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.
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}); });
document.getElementById('endBtn').onclick=()=>{ws.send(JSON.stringify({type:'end-session',sessionId,reason:'agent-ended'}));};
function esc(s){return String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));}
</script>