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
+4 -2
View File
@@ -937,13 +937,15 @@ route('POST', '/api/meetings/token', async (req, res) => {
// meeting, so a token can't be created for an arbitrary/expired code. Identity is a throwaway guest id.
route('POST', '/api/meetings/guest-token', async (req, res) => {
if (!LIVEKIT_ENABLED) return json(res, 501, { error: 'sfu not configured' });
const { room, name } = await readBody(req);
const { room, name, identity } = await readBody(req);
const rm = String(room || '').trim();
if (!/^\d{6}$/.test(rm)) return json(res, 400, { error: 'invalid room' });
const live = (() => { try { return require('./presence').meetingRooms.has(rm); } catch (_) { return false; } })();
const sched = (() => { try { const s = R.scheduledMeetings.byCode(rm); return !!(s && !s.ended_at); } catch (_) { return false; } })();
if (!live && !sched) return json(res, 404, { error: 'meeting not found or not active' });
const gid = 'guest-' + crypto.randomBytes(8).toString('hex');
// Reuse the guest's client id as the LiveKit identity so it matches the id they announced over
// signaling (meeting-join guestId) — that mapping is how their media attaches to their tile.
const gid = (typeof identity === 'string' && /^guest-[a-z0-9]+$/i.test(identity)) ? identity.slice(0, 64) : ('guest-' + crypto.randomBytes(8).toString('hex'));
const gname = String(name || 'Guest').slice(0, 60);
const token = livekitToken(gid, gname, rm, JSON.stringify({ guest: true }));
json(res, 200, { token, url: LIVEKIT_URL, identity: gid, name: gname });