feat(meetings): LiveKit SFU — phase 2 (client media plane, behind the flag)

Meeting media flows through LiveKit when the server reports sfu:true; otherwise the
P2P mesh is unchanged. Reuses the ENTIRE existing meeting UI + WS control plane
(join/host/mute/screen/recording/transcript) and only swaps the transport:
- vendored livekit-client 2.20 UMD (lazy-loaded; no build step).
- sfuInit() reads /api/meetings/config at boot; sfuConnect() joins the LiveKit room
  with the minted token after the WS meeting-join.
- remote tracks map back to the WS peerId via uid (LiveKit identity = app user id);
  per-peer stream prefers screen over camera, driving the existing sharing/stage UI.
- toggleMic/Cam/Screen publish via LiveKit; local tracks reflected into meetLocalStream
  so tiles, active-speaker meter, canvas recording and transcript keep working.
- meetMakePeer/peer-joined/peer-left/leaveMeeting branch on SFU.on; mesh path intact.

Needs a running LiveKit server + live test (phase 3 ops) to exercise end-to-end.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
SravanandClaude Opus 4.8 committed 2026-07-06 13:20:59 +05:30
1 parent f1dbcd0f86
commit 6118d59c5c
2 files changed
+87 -3

No files matched your search

+79 -3
View File
@@ -771,7 +771,7 @@
<body> <body>
<script src="/icons.js?v=4"></script> <script src="/icons.js?v=4"></script>
<script src="https://cdn.jsdelivr.net/npm/@twemoji/api@15.1.0/dist/twemoji.min.js" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/@twemoji/api@15.1.0/dist/twemoji.min.js" crossorigin="anonymous"></script>
<script>window.__BUILD='2026-07-06-batch40';console.log('%cBiz Connect','color:#1F3B73;font-weight:bold','build '+window.__BUILD); <script>window.__BUILD='2026-07-06-batch41';console.log('%cBiz Connect','color:#1F3B73;font-weight:bold','build '+window.__BUILD);
// Render modern (Twemoji) emojis in place of the OS's flat ones. No-op if the CDN didn't load // Render modern (Twemoji) emojis in place of the OS's flat ones. No-op if the CDN didn't load
// (emojis stay as plain Unicode). (#5) // (emojis stay as plain Unicode). (#5)
function twemojify(el){ try{ if(el && window.twemoji) window.twemoji.parse(el, { folder:'svg', ext:'.svg' }); }catch(_){} }</script> function twemojify(el){ try{ if(el && window.twemoji) window.twemoji.parse(el, { folder:'svg', ext:'.svg' }); }catch(_){} }</script>
@@ -2396,6 +2396,63 @@ let meetRoomTx=false; // is the room transcription active (≥1 subscrib
let meetSR=null; // my SpeechRecognition instance let meetSR=null; // my SpeechRecognition instance
let meetStageId=null; // which shared screen is currently on the stage (peerId|'__local') let meetStageId=null; // which shared screen is currently on the stage (peerId|'__local')
function meetSend(o){ try{ if(meetWs && meetWs.readyState===1) meetWs.send(JSON.stringify(o)); }catch(_){} } function meetSend(o){ try{ if(meetWs && meetWs.readyState===1) meetWs.send(JSON.stringify(o)); }catch(_){} }
// ================= LiveKit SFU media plane (feature-flagged) =================
// When the server reports sfu:true, meeting MEDIA flows through LiveKit instead of the P2P mesh:
// each client uploads once and the SFU forwards to everyone, so rooms scale past ~5. Everything
// else — the meeting WebSocket control plane (join/host/mute/screen/recording/transcript), the tile
// UI, recording and transcript — is UNCHANGED. Tiles stay keyed by the WS peerId; LiveKit tracks map
// back to a peer via its uid (LiveKit identity = app user id = the peer's uid in meetPeerUids).
let SFU={ on:false, url:'', room:null, lib:null, peers:new Map() }; // peers: pid -> {audio,cam,screen}
async function sfuInit(){ try{ const c=await fetch('/api/meetings/config').then(r=>r.json()); SFU.on=!!(c&&c.sfu); SFU.url=(c&&c.url)||''; }catch(_){ SFU.on=false; } }
function sfuLoadLib(){ return new Promise((res,rej)=>{ if(window.LivekitClient) return res(window.LivekitClient); const s=document.createElement('script'); s.src='/vendor/livekit-client.umd.min.js'; s.onload=()=>res(window.LivekitClient); s.onerror=()=>rej(new Error('livekit sdk failed to load')); document.head.appendChild(s); }); }
function peerIdForUid(uid){ for(const [pid,u] of meetPeerUids){ if(u===uid) return pid; } return null; }
async function sfuConnect(){
const LK=await sfuLoadLib(); SFU.lib=LK;
const tk=await postJSON('/api/meetings/token',{ room:meetRoom }); // per-user, per-room join credential
// adaptiveStream off: we attach media via manual srcObject (not track.attach), so LiveKit can't
// see tile visibility to pause/resume — keep all subscribed tracks flowing. dynacast stays on.
const room=new LK.Room({ adaptiveStream:false, dynacast:true }); SFU.room=room;
room.on(LK.RoomEvent.TrackSubscribed, (track,pub,participant)=>sfuAttach(pub,track,participant));
room.on(LK.RoomEvent.TrackUnsubscribed, (track,pub,participant)=>sfuDetach(pub,track,participant));
await room.connect(SFU.url, tk.token);
}
// Per-peer track bookkeeping → one tile stream that prefers the screen track over the camera,
// mirroring the mesh's single-video-per-tile model and driving the existing sharing/stage UI.
function sfuRebuild(pid){
const p=SFU.peers.get(pid)||{}; const st=new MediaStream();
const vid=p.screen||p.cam; if(vid) st.addTrack(vid); if(p.audio) st.addTrack(p.audio);
addTile(pid, st, meetNames.get(pid)||'Guest', false); setTileScreen(pid, !!p.screen); meetWatchStream(pid, st);
}
function sfuAttach(pub, track, participant){
const pid=peerIdForUid(participant.identity); if(!pid) return;
const p=SFU.peers.get(pid)||{}; SFU.peers.set(pid,p); const mt=track.mediaStreamTrack;
if(track.kind==='audio') p.audio=mt;
else if(pub.source===SFU.lib.Track.Source.ScreenShare){ p.screen=mt; meetSharers.add(pid); }
else p.cam=mt;
sfuRebuild(pid); updateShareMode();
}
function sfuDetach(pub, track, participant){
const pid=peerIdForUid(participant.identity); if(!pid) return; const p=SFU.peers.get(pid); if(!p) return; const mt=track.mediaStreamTrack;
if(p.audio===mt) p.audio=null; else if(p.screen===mt){ p.screen=null; meetSharers.delete(pid); setTileScreen(pid,false); } else if(p.cam===mt) p.cam=null;
sfuRebuild(pid); updateShareMode();
}
function sfuDropPeer(pid){ SFU.peers.delete(pid); }
// Reflect my LiveKit local mic/cam tracks into meetLocalStream so the self tile + active-speaker
// meter + canvas recording (which read tile <video> srcObjects) keep working unchanged.
function sfuSyncLocal(){
const lp=SFU.room&&SFU.room.localParticipant; if(!lp) return; const LK=SFU.lib;
const mic=lp.getTrackPublication(LK.Track.Source.Microphone), cam=lp.getTrackPublication(LK.Track.Source.Camera);
const mt=mic&&mic.track&&mic.track.mediaStreamTrack, ct=cam&&cam.track&&cam.track.mediaStreamTrack;
meetLocalStream.getTracks().forEach(t=>{ if(t!==mt&&t!==ct) meetLocalStream.removeTrack(t); });
if(mt&&meetLocalStream.getTracks().indexOf(mt)<0) meetLocalStream.addTrack(mt);
if(ct&&meetLocalStream.getTracks().indexOf(ct)<0) meetLocalStream.addTrack(ct);
addTile('__local', meetLocalStream, (ME&&ME.name)?ME.name:'You', true); meetWatchStream('__local', meetLocalStream);
}
async function sfuSetMic(on){ const lp=SFU.room&&SFU.room.localParticipant; if(!lp) return; await lp.setMicrophoneEnabled(on); sfuSyncLocal(); }
async function sfuSetCam(on){ const lp=SFU.room&&SFU.room.localParticipant; if(!lp) return; await lp.setCameraEnabled(on); sfuSyncLocal(); }
function sfuDisconnect(){ if(SFU.room){ try{ SFU.room.disconnect(); }catch(_){} SFU.room=null; } SFU.peers.clear(); }
// ================= end LiveKit SFU =================
function meetRailLive(on){ const b=document.querySelector('.railbtn[data-tab="meeting"]'); if(b) b.classList.toggle('live', !!on); } function meetRailLive(on){ const b=document.querySelector('.railbtn[data-tab="meeting"]'); if(b) b.classList.toggle('live', !!on); }
function renderMeetingLobby(){ function renderMeetingLobby(){
@@ -2639,6 +2696,8 @@ function meetWatchStream(id, stream){
function meetUnwatch(id){ const r=meetVU.get(id); if(!r) return; try{cancelAnimationFrame(r.raf);}catch(_){} try{r.src.disconnect();}catch(_){} try{r.ctx.close();}catch(_){} meetVU.delete(id); const t=document.getElementById('meet-tile-'+id); if(t) t.classList.remove('speaking'); } function meetUnwatch(id){ const r=meetVU.get(id); if(!r) return; try{cancelAnimationFrame(r.raf);}catch(_){} try{r.src.disconnect();}catch(_){} try{r.ctx.close();}catch(_){} meetVU.delete(id); const t=document.getElementById('meet-tile-'+id); if(t) t.classList.remove('speaking'); }
function meetUnwatchAll(){ for(const id of Array.from(meetVU.keys())) meetUnwatch(id); } function meetUnwatchAll(){ for(const id of Array.from(meetVU.keys())) meetUnwatch(id); }
function meetMakePeer(peerId, name){ function meetMakePeer(peerId, name){
// SFU mode: no P2P peer connection — just show the tile (avatar until LiveKit delivers media).
if(SFU.on){ addTile(peerId, null, meetNames.get(peerId)||name||'Guest', false); return null; }
const pc=new RTCPeerConnection(MEET_ICE); const pc=new RTCPeerConnection(MEET_ICE);
const entry={ pc, name, vsender:null }; meetPeers.set(peerId, entry); const entry={ pc, name, vsender:null }; meetPeers.set(peerId, entry);
addTile(peerId, null, meetNames.get(peerId)||name||'Guest', false); // show the tile right away (avatar) addTile(peerId, null, meetNames.get(peerId)||name||'Guest', false); // show the tile right away (avatar)
@@ -2655,6 +2714,15 @@ function meetMakePeer(peerId, name){
async function toggleScreen(){ async function toggleScreen(){
if(meetScreen){ stopScreen(); return; } if(meetScreen){ stopScreen(); return; }
if(!meetMultiShare && meetSharers.size>0){ toast('Someone is already sharing their screen'); return; } if(!meetMultiShare && meetSharers.size>0){ toast('Someone is already sharing their screen'); return; }
if(SFU.on){
try{ await SFU.room.localParticipant.setScreenShareEnabled(true); }catch(e){ return; } // user cancelled / denied
const pub=SFU.room.localParticipant.getTrackPublication(SFU.lib.Track.Source.ScreenShare);
const st=pub&&pub.track&&pub.track.mediaStreamTrack; if(!st){ return; }
meetScreen=true; meetScreenStream=new MediaStream([st]);
try{ st.addEventListener('ended', ()=>stopScreen()); }catch(_){} // browser's native "Stop sharing" bar
addTile('__local', meetScreenStream, ((ME&&ME.name)?ME.name:'You')+' (screen)', true); setTileScreen('__local', true);
updateScreenBtn(); meetSend({ type:'meeting-screen', on:true }); return;
}
let ds; try{ ds=await navigator.mediaDevices.getDisplayMedia({ video:true, audio:false }); } let ds; try{ ds=await navigator.mediaDevices.getDisplayMedia({ video:true, audio:false }); }
catch(e){ return; } // user cancelled the picker catch(e){ return; } // user cancelled the picker
const track=ds.getVideoTracks()[0]; if(!track){ try{ ds.getTracks().forEach(t=>t.stop()); }catch(_){} return; } const track=ds.getVideoTracks()[0]; if(!track){ try{ ds.getTracks().forEach(t=>t.stop()); }catch(_){} return; }
@@ -2669,6 +2737,7 @@ async function toggleScreen(){
} }
function stopScreen(){ function stopScreen(){
if(!meetScreen) return; meetScreen=false; if(!meetScreen) return; meetScreen=false;
if(SFU.on){ try{ SFU.room.localParticipant.setScreenShareEnabled(false); }catch(_){} meetScreenStream=null; addTile('__local', meetLocalStream, (ME&&ME.name)?ME.name:'You', true); setTileScreen('__local', false); updateScreenBtn(); meetSend({ type:'meeting-screen', on:false }); return; }
const cam=meetLocalStream && meetLocalStream.getVideoTracks()[0]; const cam=meetLocalStream && meetLocalStream.getVideoTracks()[0];
for(const [,p] of meetPeers){ if(p.vsender){ try{ p.vsender.replaceTrack(cam||null); }catch(_){} } } for(const [,p] of meetPeers){ if(p.vsender){ try{ p.vsender.replaceTrack(cam||null); }catch(_){} } }
if(meetScreenStream){ try{ meetScreenStream.getTracks().forEach(t=>t.stop()); }catch(_){} meetScreenStream=null; } if(meetScreenStream){ try{ meetScreenStream.getTracks().forEach(t=>t.stop()); }catch(_){} meetScreenStream=null; }
@@ -2788,6 +2857,9 @@ async function onMeetMsg(e){
// Existing peers OFFER to me (their offers carry their tracks incl. any active screen share); // Existing peers OFFER to me (their offers carry their tracks incl. any active screen share);
// I just set up the connections and wait. Avoids the "newcomer can't receive screen" bug. // I just set up the connections and wait. Avoids the "newcomer can't receive screen" bug.
for(const p of (m.peers||[])){ meetNames.set(p.peerId,p.name); if(p.avatar) meetAvatars.set(p.peerId,p.avatar); if(p.uid){ meetPeerUids.set(p.peerId,p.uid); meetInviteJoined(p.uid); } meetMakePeer(p.peerId,p.name); } for(const p of (m.peers||[])){ meetNames.set(p.peerId,p.name); if(p.avatar) meetAvatars.set(p.peerId,p.avatar); if(p.uid){ meetPeerUids.set(p.peerId,p.uid); meetInviteJoined(p.uid); } meetMakePeer(p.peerId,p.name); }
// SFU: connect to LiveKit for media once (peer uid→peerId map is populated above). Mic/cam are
// off at join, so nothing publishes yet — toggleMic/toggleCam publish on demand.
if(SFU.on){ try{ await sfuConnect(); }catch(err){ const e2=document.getElementById('meetErr'); if(e2) e2.textContent='Could not connect meeting media'; } }
meetSend({type:'meeting-state', muted:!meetMic, camOff:!meetCam}); // tell existing peers my state meetSend({type:'meeting-state', muted:!meetMic, camOff:!meetCam}); // tell existing peers my state
if(meetIsHost) meetSend({type:'meeting-host', to:meetMyId}); // announce host so others know if(meetIsHost) meetSend({type:'meeting-host', to:meetMyId}); // announce host so others know
refreshMeetPanel(); updateHostControls(); refreshMeetPanel(); updateHostControls();
@@ -2797,7 +2869,7 @@ async function onMeetMsg(e){
if(m.type==='meeting-peer-joined'){ if(m.type==='meeting-peer-joined'){
meetNames.set(m.peerId,m.name); if(m.avatar) meetAvatars.set(m.peerId,m.avatar); if(m.uid){ meetPeerUids.set(m.peerId,m.uid); meetInviteJoined(m.uid); } meetNames.set(m.peerId,m.name); if(m.avatar) meetAvatars.set(m.peerId,m.avatar); if(m.uid){ meetPeerUids.set(m.peerId,m.uid); meetInviteJoined(m.uid); }
const pc=meetMakePeer(m.peerId,m.name); // I'm an existing peer → I OFFER to the newcomer (carries my screen) const pc=meetMakePeer(m.peerId,m.name); // I'm an existing peer → I OFFER to the newcomer (carries my screen)
try{ const offer=await pc.createOffer(); await pc.setLocalDescription(offer); meetSend({type:'meeting-signal',to:m.peerId,data:{sdp:pc.localDescription}}); }catch(_){} if(pc){ try{ const offer=await pc.createOffer(); await pc.setLocalDescription(offer); meetSend({type:'meeting-signal',to:m.peerId,data:{sdp:pc.localDescription}}); }catch(_){} } // (SFU: LiveKit handles media, no offer)
meetSend({type:'meeting-state', muted:!meetMic, camOff:!meetCam}); meetSend({type:'meeting-state', muted:!meetMic, camOff:!meetCam});
if(meetIsHost){ meetSend({type:'meeting-host', to:meetHostId}); meetSend({type:'meeting-sharemode', multi:meetMultiShare}); if(meetRec) meetSend({type:'meeting-recording', on:true}); } if(meetIsHost){ meetSend({type:'meeting-host', to:meetHostId}); meetSend({type:'meeting-sharemode', multi:meetMultiShare}); if(meetRec) meetSend({type:'meeting-recording', on:true}); }
if(meetScreen) meetSend({type:'meeting-screen', on:true}); if(meetScreen) meetSend({type:'meeting-screen', on:true});
@@ -2810,7 +2882,7 @@ async function onMeetMsg(e){
if(m.type==='meeting-peer-screen'){ if(m.on) meetSharers.add(m.from); else meetSharers.delete(m.from); setTileScreen(m.from, !!m.on); refreshMeetPanel(); return; } if(m.type==='meeting-peer-screen'){ if(m.on) meetSharers.add(m.from); else meetSharers.delete(m.from); setTileScreen(m.from, !!m.on); refreshMeetPanel(); return; }
if(m.type==='meeting-sharemode'){ meetMultiShare=!!m.multi; refreshMeetPanel(); return; } if(m.type==='meeting-sharemode'){ meetMultiShare=!!m.multi; refreshMeetPanel(); return; }
if(m.type==='meeting-muteall'){ if(meetMic && meetLocalStream){ meetMic=false; meetLocalStream.getAudioTracks().forEach(t=>t.enabled=false); updateMicBtn(); setTileMute('__local', true); meetSend({type:'meeting-state', muted:true, camOff:!meetCam}); } toast('You were muted by the host'); return; } if(m.type==='meeting-muteall'){ if(meetMic && meetLocalStream){ meetMic=false; meetLocalStream.getAudioTracks().forEach(t=>t.enabled=false); updateMicBtn(); setTileMute('__local', true); meetSend({type:'meeting-state', muted:true, camOff:!meetCam}); } toast('You were muted by the host'); return; }
if(m.type==='meeting-peer-left'){ const p=meetPeers.get(m.peerId); if(p){ try{p.pc.close();}catch(_){} meetPeers.delete(m.peerId);} meetSharers.delete(m.peerId); removeTile(m.peerId); refreshMeetPanel(); return; } if(m.type==='meeting-peer-left'){ const p=meetPeers.get(m.peerId); if(p){ try{p.pc.close();}catch(_){} meetPeers.delete(m.peerId);} if(SFU.on) sfuDropPeer(m.peerId); meetSharers.delete(m.peerId); removeTile(m.peerId); refreshMeetPanel(); return; }
if(m.type==='meeting-signal'){ if(m.type==='meeting-signal'){
const from=m.from, d=m.data||{}; const from=m.from, d=m.data||{};
if(d.sdp){ if(d.sdp){
@@ -2829,6 +2901,7 @@ function updateCamBtn(){ const b=document.getElementById('meetCamBtn'); if(b){ b
// Unmute acquires the mic on demand (no prompt until then) and renegotiates with peers. // Unmute acquires the mic on demand (no prompt until then) and renegotiates with peers.
async function toggleMic(){ async function toggleMic(){
if(!meetLocalStream) return; if(!meetLocalStream) return;
if(SFU.on){ const next=!meetMic; try{ await sfuSetMic(next); meetMic=next; }catch(e){ toast('Microphone permission is required to unmute'); return; } updateMicBtn(); setTileMute('__local', !meetMic); meetSend({type:'meeting-state', muted:!meetMic, camOff:!meetCam}); return; }
const hasTrack=meetLocalStream.getAudioTracks().length>0; const hasTrack=meetLocalStream.getAudioTracks().length>0;
if(!hasTrack){ if(!hasTrack){
let astream; try{ astream=await navigator.mediaDevices.getUserMedia({ audio:true }); } let astream; try{ astream=await navigator.mediaDevices.getUserMedia({ audio:true }); }
@@ -2845,6 +2918,7 @@ async function toggleMic(){
// renegotiates with every peer, so you can always turn video on once a meeting has started. // renegotiates with every peer, so you can always turn video on once a meeting has started.
async function toggleCam(){ async function toggleCam(){
if(!meetLocalStream) return; if(!meetLocalStream) return;
if(SFU.on){ const next=!meetCam; try{ await sfuSetCam(next); meetCam=next; meetAudioOnly=false; }catch(e){ toast('Camera permission is required to turn on video'); return; } updateCamBtn(); addTile('__local', meetLocalStream, (ME&&ME.name)?ME.name:'You', true); setTileMute('__local', !meetMic); meetSend({type:'meeting-state', muted:!meetMic, camOff:!meetCam}); return; }
const hasTrack=meetLocalStream.getVideoTracks().length>0; const hasTrack=meetLocalStream.getVideoTracks().length>0;
if(!hasTrack){ if(!hasTrack){
let vstream; try{ vstream=await navigator.mediaDevices.getUserMedia({ video:true }); } let vstream; try{ vstream=await navigator.mediaDevices.getUserMedia({ video:true }); }
@@ -2878,6 +2952,7 @@ function leaveMeeting(forced){
if(meetScreen) stopScreen(); if(meetScreen) stopScreen();
if(_recTimer){ clearInterval(_recTimer); _recTimer=null; } const _rn=document.getElementById('recNotice'); if(_rn) _rn.remove(); // clear any "Recording" badge if(_recTimer){ clearInterval(_recTimer); _recTimer=null; } const _rn=document.getElementById('recNotice'); if(_rn) _rn.remove(); // clear any "Recording" badge
meetSend({type:'meeting-leave'}); meetSend({type:'meeting-leave'});
if(SFU.on) sfuDisconnect(); // tear down the LiveKit room
meetUnwatchAll(); meetSharers.clear(); meetUnwatchAll(); meetSharers.clear();
meetPeers.forEach(p=>{ try{p.pc.close();}catch(_){} }); meetPeers.clear(); meetNames.clear(); meetAvatars.clear(); meetPeerUids.clear(); meetCamOff.clear(); meetInvited.forEach(e=>{ if(e.timer) clearTimeout(e.timer); }); meetInvited.clear(); meetMuted.clear(); meetPeers.forEach(p=>{ try{p.pc.close();}catch(_){} }); meetPeers.clear(); meetNames.clear(); meetAvatars.clear(); meetPeerUids.clear(); meetCamOff.clear(); meetInvited.forEach(e=>{ if(e.timer) clearTimeout(e.timer); }); meetInvited.clear(); meetMuted.clear();
if(meetLocalStream){ try{ meetLocalStream.getTracks().forEach(t=>t.stop()); }catch(_){} meetLocalStream=null; } if(meetLocalStream){ try{ meetLocalStream.getTracks().forEach(t=>t.stop()); }catch(_){} meetLocalStream=null; }
@@ -3038,6 +3113,7 @@ async function doRegister(){
loadNotifs(); wireBell(); wireProfile(); loadNotifs(); wireBell(); wireProfile();
placeHdrRight(); window.addEventListener('resize', placeHdrRight); // mobile: bell+profile live in the chat-list header (no top bar) placeHdrRight(); window.addEventListener('resize', placeHdrRight); // mobile: bell+profile live in the chat-list header (no top bar)
connectChatWs(); connectChatWs();
sfuInit(); // learn whether meetings use the LiveKit SFU or the P2P mesh (before any call starts)
reportInstall(); // desktop/mobile shell: record this install against the signed-in user reportInstall(); // desktop/mobile shell: record this install against the signed-in user
setupPush(); // register the notification service worker + subscribe to Web Push (if granted) setupPush(); // register the notification service worker + subscribe to Web Push (if granted)
{ const cl=document.getElementById('chatlist'); if(cl) enablePullRefresh(cl, loadSidebar); } // pull-to-refresh the chat list { const cl=document.getElementById('chatlist'); if(cl) enablePullRefresh(cl, loadSidebar); } // pull-to-refresh the chat list
+8
View File
@@ -0,0 +1,8 @@
/**
* Skipped minification because the original files appears to be already minified.
* Original file: /npm/livekit-client@2.20.0/dist/livekit-client.umd.js
*
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
*/
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).LivekitClient={})}(this,(function(e){"use strict";function t(e,t){return t.forEach((function(t){t&&"string"!=typeof t&&!Array.isArray(t)&&Object.keys(t).forEach((function(n){if("default"!==n&&!(n in e)){var i=Object.getOwnPropertyDescriptor(t,n);Object.defineProperty(e,n,i.get?i:{enumerable:!0,get:function(){return t[n]}})}}))})),Object.freeze(e)}var n=Object.defineProperty,i=(e,t,i)=>((e,t,i)=>t in e?n(e,t,{enumerable:!0,configurable:!0,writable:!0,value:i}):e[t]=i)(e,"symbol"!=typeof t?t+"":t,i);class r{constructor(){i(this,"_locking"),i(this,"_locks"),this._locking=Promise.resolve(),this._locks=0}isLocked(){return this._locks>0}lock(){let e;this._locks+=1;const t=new Promise((t=>e=()=>{this._locks-=1,t()})),n=this._locking.then((()=>e));return this._locking=this._locking.then((()=>t)),n}}function a(e,t){if(!e)throw new Error(t)}function s(e){if("number"!=typeof e)throw new Error("invalid int 32: "+typeof e);if(!Number.isInteger(e)||e>2147483647||e<-2147483648)throw new Error("invalid int 32: "+e)}function o(e){if("number"!=typeof e)throw new Error("invalid uint 32: "+typeof e);if(!Number.isInteger(e)||e>4294967295||e<0)throw new Error("invalid uint 32: "+e)}function c(e){if("number"!=typeof e)throw new Error("invalid float 32: "+typeof e);if(Number.isFinite(e)&&(e>34028234663852886e22||e<-34028234663852886e22))throw new Error("invalid float 32: "+e)}const d=Symbol("@bufbuild/protobuf/enum-type");function l(e){const t=e[d];return a(t,"missing enum type on enum object"),t}function u(e,t,n,i){e[d]=h(t,n.map((t=>({no:t.no,name:t.name,localName:e[t.no]}))))}function h(e,t,n){const i=Object.create(null),r=Object.create(null),a=[];for(const e of t){const t=m(e);a.push(t),i[e.name]=t,r[e.no]=t}return{typeName:e,values:a,findName:e=>i[e],findNumber:e=>r[e]}}function p(e,t,n){const i={};for(const e of t){const t=m(e);i[t.localName]=t.no,i[t.no]=t.localName}return u(i,e,t),i}function m(e){return"localName"in e?e:Object.assign(Object.assign({},e),{localName:e.name})}class g{equals(e){return this.getType().runtime.util.equals(this.getType(),this,e)}clone(){return this.getType().runtime.util.clone(this)}fromBinary(e,t){const n=this.getType().runtime.bin,i=n.makeReadOptions(t);return n.readMessage(this,i.readerFactory(e),e.byteLength,i),this}fromJson(e,t){const n=this.getType(),i=n.runtime.json,r=i.makeReadOptions(t);return i.readMessage(n,e,r,this),this}fromJsonString(e,t){let n;try{n=JSON.parse(e)}catch(e){throw new Error("cannot decode ".concat(this.getType().typeName," from JSON: ").concat(e instanceof Error?e.message:String(e)))}return this.fromJson(n,t)}toBinary(e){const t=this.getType().runtime.bin,n=t.makeWriteOptions(e),i=n.writerFactory();return t.writeMessage(this,i,n),i.finish()}toJson(e){const t=this.getType().runtime.json,n=t.makeWriteOptions(e);return t.writeMessage(this,n)}toJsonString(e){var t;const n=this.toJson(e);return JSON.stringify(n,null,null!==(t=null==e?void 0:e.prettySpaces)&&void 0!==t?t:0)}toJSON(){return this.toJson({emitDefaultValues:!0})}getType(){return Object.getPrototypeOf(this).constructor}}function v(){let e=0,t=0;for(let n=0;n<28;n+=7){let i=this.buf[this.pos++];if(e|=(127&i)<<n,!(128&i))return this.assertBounds(),[e,t]}let n=this.buf[this.pos++];if(e|=(15&n)<<28,t=(112&n)>>4,!(128&n))return this.assertBounds(),[e,t];for(let n=3;n<=31;n+=7){let i=this.buf[this.pos++];if(t|=(127&i)<<n,!(128&i))return this.assertBounds(),[e,t]}throw new Error("invalid varint")}function f(e,t,n){for(let i=0;i<28;i+=7){const r=e>>>i,a=!(r>>>7==0&&0==t),s=255&(a?128|r:r);if(n.push(s),!a)return}const i=e>>>28&15|(7&t)<<4,r=!!(t>>3);if(n.push(255&(r?128|i:i)),r){for(let e=3;e<31;e+=7){const i=t>>>e,r=!(i>>>7==0),a=255&(r?128|i:i);if(n.push(a),!r)return}n.push(t>>>31&1)}}const k=4294967296;function y(e){const t="-"===e[0];t&&(e=e.slice(1));const n=1e6;let i=0,r=0;function a(t,a){const s=Number(e.slice(t,a));r*=n,i=i*n+s,i>=k&&(r+=i/k|0,i%=k)}return a(-24,-18),a(-18,-12),a(-12,-6),a(-6),t?S(i,r):T(i,r)}function b(e,t){var n=function(e,t){return{lo:e>>>0,hi:t>>>0}}(e,t);if(e=n.lo,(t=n.hi)<=2097151)return String(k*t+e);const i=16777215&(e>>>24|t<<8),r=t>>16&65535;let a=(16777215&e)+6777216*i+6710656*r,s=i+8147497*r,o=2*r;const c=1e7;return a>=c&&(s+=Math.floor(a/c),a%=c),s>=c&&(o+=Math.floor(s/c),s%=c),o.toString()+E(s)+E(a)}function T(e,t){return{lo:0|e,hi:0|t}}function S(e,t){return t=~t,e?e=1+~e:t+=1,T(e,t)}const E=e=>{const t=String(e);return"0000000".slice(t.length)+t};function C(e,t){if(e>=0){for(;e>127;)t.push(127&e|128),e>>>=7;t.push(e)}else{for(let n=0;n<9;n++)t.push(127&e|128),e>>=7;t.push(1)}}function w(){let e=this.buf[this.pos++],t=127&e;if(!(128&e))return this.assertBounds(),t;if(e=this.buf[this.pos++],t|=(127&e)<<7,!(128&e))return this.assertBounds(),t;if(e=this.buf[this.pos++],t|=(127&e)<<14,!(128&e))Line truncated
//# sourceMappingURL=livekit-client.umd.js.map