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:
1 parent
f1dbcd0f86
commit
6118d59c5c
2 files changed
+87
-3
No files matched your search
+79
-3
@@ -771,7 +771,7 @@
|
||||
<body>
|
||||
<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>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
|
||||
// (emojis stay as plain Unicode). (#5)
|
||||
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 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(_){} }
|
||||
|
||||
// ================= 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 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 meetUnwatchAll(){ for(const id of Array.from(meetVU.keys())) meetUnwatch(id); }
|
||||
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 entry={ pc, name, vsender:null }; meetPeers.set(peerId, entry);
|
||||
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(){
|
||||
if(meetScreen){ stopScreen(); 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 }); }
|
||||
catch(e){ return; } // user cancelled the picker
|
||||
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(){
|
||||
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];
|
||||
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; }
|
||||
@@ -2788,6 +2857,9 @@ async function onMeetMsg(e){
|
||||
// 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.
|
||||
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
|
||||
if(meetIsHost) meetSend({type:'meeting-host', to:meetMyId}); // announce host so others know
|
||||
refreshMeetPanel(); updateHostControls();
|
||||
@@ -2797,7 +2869,7 @@ async function onMeetMsg(e){
|
||||
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); }
|
||||
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});
|
||||
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});
|
||||
@@ -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-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-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'){
|
||||
const from=m.from, d=m.data||{};
|
||||
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.
|
||||
async function toggleMic(){
|
||||
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;
|
||||
if(!hasTrack){
|
||||
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.
|
||||
async function toggleCam(){
|
||||
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;
|
||||
if(!hasTrack){
|
||||
let vstream; try{ vstream=await navigator.mediaDevices.getUserMedia({ video:true }); }
|
||||
@@ -2878,6 +2952,7 @@ function leaveMeeting(forced){
|
||||
if(meetScreen) stopScreen();
|
||||
if(_recTimer){ clearInterval(_recTimer); _recTimer=null; } const _rn=document.getElementById('recNotice'); if(_rn) _rn.remove(); // clear any "Recording" badge
|
||||
meetSend({type:'meeting-leave'});
|
||||
if(SFU.on) sfuDisconnect(); // tear down the LiveKit room
|
||||
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();
|
||||
if(meetLocalStream){ try{ meetLocalStream.getTracks().forEach(t=>t.stop()); }catch(_){} meetLocalStream=null; }
|
||||
@@ -3038,6 +3113,7 @@ async function doRegister(){
|
||||
loadNotifs(); wireBell(); wireProfile();
|
||||
placeHdrRight(); window.addEventListener('resize', placeHdrRight); // mobile: bell+profile live in the chat-list header (no top bar)
|
||||
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
|
||||
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
|
||||
|
||||
@@ -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
|
||||
Reference in new issue
Block a user