|
|
@@ -39,6 +39,10 @@
|
|
39
|
39
|
.profile .pmenu a{display:block;padding:.6rem .9rem;color:#1f2430;text-decoration:none;font-size:.9rem;cursor:pointer}
|
|
40
|
40
|
.profile .pmenu a:hover{background:#f1f5f9}
|
|
41
|
41
|
.profile .pmenu a.danger{color:#b91c1c;border-top:1px solid #eef1f6}
|
|
|
42
|
+ .pwwrap{position:relative;}
|
|
|
43
|
+ .pwwrap input{padding-right:2.7rem;}
|
|
|
44
|
+ .eye{position:absolute;right:.5rem;top:50%;transform:translateY(-50%);background:none;border:none;padding:.3rem;width:auto;color:var(--muted);display:inline-flex;align-items:center;cursor:pointer;margin:0;}
|
|
|
45
|
+ .eye:hover{color:var(--blue);}
|
|
42
|
46
|
</style>
|
|
43
|
47
|
</head>
|
|
44
|
48
|
<body>
|
|
|
@@ -52,7 +56,8 @@
|
|
52
|
56
|
|
|
53
|
57
|
<script>
|
|
54
|
58
|
let ICE={iceServers:[{urls:'stun:stun.l.google.com:19302'}]};
|
|
55
|
|
-let __icePromise=Promise.resolve();try{__icePromise=fetch('/api/ice').then(r=>r.ok?r.json():null).then(c=>{if(c&&c.iceServers)ICE=c;}).catch(()=>{});}catch(_){}
|
|
|
59
|
+const IS_MOBILE=/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile/i.test(navigator.userAgent||'');
|
|
|
60
|
+let __icePromise=Promise.resolve();try{__icePromise=fetch('/api/ice').then(r=>r.ok?r.json():null).then(c=>{if(c&&c.iceServers&&IS_MOBILE)ICE=c;}).catch(()=>{});}catch(_){}
|
|
56
|
61
|
async function ensureIce(){try{await __icePromise;}catch(_){}return ICE;}
|
|
57
|
62
|
function pEsc(s){return String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));}
|
|
58
|
63
|
function profileHTML(name){return '<div class="profile"><button class="pbtn" id="pbtn">'+pEsc(name)+' <span style="font-size:.65rem">▾</span></button><div class="pmenu" id="pmenu"><a href="/console">Console / Dashboard</a><a id="plogout" class="danger">Logout</a></div></div>';}
|
|
|
@@ -82,13 +87,16 @@ function onEnter(ids, fn){ ids.forEach(id => { const el=document.getElementById(
|
|
82
|
87
|
})();
|
|
83
|
88
|
|
|
84
|
89
|
// ---- LOGIN ----
|
|
|
90
|
+const EYE_OFF='<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/><line x1="1" y1="1" x2="23" y2="23"/></svg>';
|
|
|
91
|
+const EYE_ON='<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>';
|
|
|
92
|
+function wireEyes(){document.querySelectorAll('.eye').forEach(b=>{if(b._w)return;b._w=1;b.innerHTML=EYE_OFF;b.onclick=()=>{const inp=document.getElementById(b.getAttribute('data-for'));if(!inp)return;const show=inp.type==='password';inp.type=show?'text':'password';b.innerHTML=show?EYE_ON:EYE_OFF;};});}
|
|
85
|
93
|
function renderLogin(){
|
|
86
|
94
|
agentChip.textContent='';
|
|
87
|
95
|
card.innerHTML = `
|
|
88
|
|
- <h1>Agent sign in</h1>
|
|
|
96
|
+ <h1>Sign in</h1>
|
|
89
|
97
|
<div class="sub">Sign in to start a support session. Your name is shown to the customer.</div>
|
|
90
|
98
|
<span class="lbl">Email</span><input id="email" type="email" placeholder="you@bizgaze.com">
|
|
91
|
|
- <span class="lbl">Password</span><input id="pw" type="password" placeholder="password">
|
|
|
99
|
+ <span class="lbl">Password</span><div class="pwwrap"><input id="pw" type="password" placeholder="password"><button type="button" class="eye" data-for="pw" aria-label="Show password"></button></div>
|
|
92
|
100
|
<label style="display:flex;align-items:center;gap:.5rem;margin:.7rem 0;color:var(--ink);font-size:.9rem;cursor:pointer"><input type="checkbox" id="rememberMe" style="width:18px;height:18px;accent-color:var(--blue);margin:0"> Remember me on this device</label>
|
|
93
|
101
|
<button class="btn" id="loginBtn" style="width:100%">Sign in</button>
|
|
94
|
102
|
<div class="status err" id="err"></div>`;
|
|
|
@@ -101,6 +109,7 @@ function renderLogin(){
|
|
101
|
109
|
};
|
|
102
|
110
|
document.getElementById('loginBtn').onclick=doSignIn;
|
|
103
|
111
|
onEnter(['email','pw'], doSignIn);
|
|
|
112
|
+ wireEyes();
|
|
104
|
113
|
}
|
|
105
|
114
|
}
|
|
106
|
115
|
|
|
|
@@ -143,6 +152,7 @@ function connectWS(){
|
|
143
|
152
|
const ans=await pc.createAnswer(); await pc.setLocalDescription(ans);
|
|
144
|
153
|
ws.send(JSON.stringify({type:'answer',sessionId,sdp:pc.localDescription})); break;
|
|
145
|
154
|
case 'ice-candidate': if(m.candidate&&pc) await pc.addIceCandidate(new RTCIceCandidate(m.candidate)); break;
|
|
|
155
|
+ case 'transcript': if(recogActive&&m.text) addLine('customer', m.name||'Customer', m.text, !!m.chat); break;
|
|
146
|
156
|
case 'session-denied': renderEnded('The customer declined the request.'); break;
|
|
147
|
157
|
case 'session-ended': {
|
|
148
|
158
|
const msgs={'share-cancelled':'The customer cancelled the screen selection. Ask them to refresh their page for a new code.',
|
|
|
@@ -163,6 +173,7 @@ function renderWaiting(){
|
|
163
|
173
|
}
|
|
164
|
174
|
|
|
165
|
175
|
function renderEnded(msg){
|
|
|
176
|
+ try{ stopRecording(); }catch(_){}
|
|
166
|
177
|
removeSessionUI();
|
|
167
|
178
|
if(pc){ try{pc.close();}catch(e){} pc=null; }
|
|
168
|
179
|
video.style.display='none'; bar.classList.remove('show');
|
|
|
@@ -180,6 +191,89 @@ const SVG_MIC='<svg viewBox="0 0 24 24" width="18" height="18" fill="none" strok
|
|
180
|
191
|
const SVG_MICOFF='<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="3" y1="3" x2="21" y2="21"/><path d="M9 9v4a3 3 0 0 0 5 2.2"/><path d="M5 10a7 7 0 0 0 10.9 5.6"/><line x1="12" y1="19" x2="12" y2="22"/></svg>';
|
|
181
|
192
|
const SVG_CHAT='<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>';
|
|
182
|
193
|
const SVG_END='<svg viewBox="0 0 24 24" width="17" height="17" fill="#fff"><rect x="5" y="5" width="14" height="14" rx="2.5"/></svg>';
|
|
|
194
|
+const SVG_REC='<svg viewBox="0 0 24 24" width="16" height="16"><circle cx="12" cy="12" r="7" fill="#ef4444"/></svg>';
|
|
|
195
|
+const SVG_RECSTOP='<svg viewBox="0 0 24 24" width="15" height="15" fill="#fff"><rect x="6" y="6" width="12" height="12" rx="2"/></svg>';
|
|
|
196
|
+let mediaRecorder=null, recChunks=[], recCtx=null;
|
|
|
197
|
+const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
|
|
|
198
|
+let recog=null, recogActive=false, transcriptLines=[];
|
|
|
199
|
+let recTimerInt=null, recStartTs=0;
|
|
|
200
|
+function fmtElapsedA(ms){const s=Math.max(0,Math.floor(ms/1000));return String(Math.floor(s/60)).padStart(2,'0')+':'+String(s%60).padStart(2,'0');}
|
|
|
201
|
+function showRecTimer(on){
|
|
|
202
|
+ let c=document.getElementById('recTimer');
|
|
|
203
|
+ if(on){
|
|
|
204
|
+ if(!c){ c=document.createElement('div'); c.id='recTimer';
|
|
|
205
|
+ c.style.cssText='display:inline-flex;align-items:center;gap:6px;color:#fff;font-weight:700;font-size:.85rem;background:#dc2626;padding:.5rem .7rem;border-radius:12px';
|
|
|
206
|
+ c.innerHTML='<span style="width:9px;height:9px;border-radius:50%;background:#fff;display:inline-block;animation:recpulseA 1.2s infinite"></span><span id="recTimerVal">00:00</span>';
|
|
|
207
|
+ const bar=document.getElementById('sessionBar'), rb=document.getElementById('recBtn');
|
|
|
208
|
+ if(bar&&rb){ bar.insertBefore(c, rb); } else if(bar){ bar.appendChild(c); }
|
|
|
209
|
+ if(!document.getElementById('recPulseStyleA')){const st=document.createElement('style');st.id='recPulseStyleA';st.textContent='@keyframes recpulseA{0%,100%{opacity:1}50%{opacity:.2}}';document.head.appendChild(st);}
|
|
|
210
|
+ }
|
|
|
211
|
+ recStartTs=Date.now(); clearInterval(recTimerInt);
|
|
|
212
|
+ const upd=()=>{ const v=document.getElementById('recTimerVal'); if(v) v.textContent=fmtElapsedA(Date.now()-recStartTs); };
|
|
|
213
|
+ upd(); recTimerInt=setInterval(upd,1000);
|
|
|
214
|
+ } else { clearInterval(recTimerInt); recTimerInt=null; if(c) c.remove(); }
|
|
|
215
|
+}
|
|
|
216
|
+function addLine(role, name, text, isChat){ transcriptLines.push({ t: Date.now(), role: role, name: name||'', text: text, chat: !!isChat }); }
|
|
|
217
|
+function startTranscription(){
|
|
|
218
|
+ transcriptLines=[];
|
|
|
219
|
+ if(!SR) return;
|
|
|
220
|
+ try{
|
|
|
221
|
+ recog=new SR(); recog.continuous=true; recog.interimResults=false; recog.lang='en-US';
|
|
|
222
|
+ recog.onresult=(e)=>{ for(let i=e.resultIndex;i<e.results.length;i++){ if(e.results[i].isFinal){ const txt=(e.results[i][0].transcript||'').trim(); if(txt) addLine('agent',(me&&(me.name||me.email))||'Agent',txt,false); } } };
|
|
|
223
|
+ recog.onerror=()=>{};
|
|
|
224
|
+ recog.onend=()=>{ if(recogActive){ try{recog.start();}catch(_){} } };
|
|
|
225
|
+ recogActive=true; recog.start();
|
|
|
226
|
+ }catch(e){}
|
|
|
227
|
+}
|
|
|
228
|
+function stopTranscription(){ recogActive=false; if(recog){ try{recog.stop();}catch(_){} recog=null; } }
|
|
|
229
|
+function buildTranscriptText(){
|
|
|
230
|
+ const lines=transcriptLines.slice().sort((a,b)=>a.t-b.t);
|
|
|
231
|
+ const pad=(n)=>String(n).padStart(2,'0');
|
|
|
232
|
+ const head='BizGaze Support — Session transcript\nSession: '+sessionId+'\nGenerated: '+new Date().toLocaleString()+'\n'+('-'.repeat(48))+'\n';
|
|
|
233
|
+ const body=lines.map(l=>{ const d=new Date(l.t); const ts='['+pad(d.getHours())+':'+pad(d.getMinutes())+':'+pad(d.getSeconds())+']'; const who=(l.role==='agent'?'Agent':'Customer')+(l.name?' ('+l.name+')':'')+(l.chat?' [chat]':''); return ts+' '+who+': '+l.text; }).join('\n');
|
|
|
234
|
+ return head+(body||'(no speech captured)')+'\n';
|
|
|
235
|
+}
|
|
|
236
|
+async function uploadTranscript(){
|
|
|
237
|
+ if(!transcriptLines.length) return;
|
|
|
238
|
+ try{ await fetch('/api/transcript?sessionId='+encodeURIComponent(sessionId),{method:'POST',headers:{'Content-Type':'text/plain'},body:buildTranscriptText()}); }catch(_){}
|
|
|
239
|
+}
|
|
|
240
|
+function recBtnUpdate(on){const b=document.getElementById('recBtn');if(!b)return;b.innerHTML='<span style="display:inline-flex">'+(on?SVG_RECSTOP:SVG_REC)+'</span>';b.title=on?'Stop recording':'Record';b.style.background=on?'#dc2626':'#0ea5e9';}
|
|
|
241
|
+function startRecording(){
|
|
|
242
|
+ const remote=video.srcObject;
|
|
|
243
|
+ const _vt=remote&&remote.getVideoTracks&&remote.getVideoTracks()[0];
|
|
|
244
|
+ if(!_vt||_vt.readyState!=='live'){ alert('No live screen to record. The customer may have disconnected.'); return; }
|
|
|
245
|
+ if(pc&&pc.connectionState&&pc.connectionState!=='connected'){ alert('Not connected to the customer right now.'); return; }
|
|
|
246
|
+ try{
|
|
|
247
|
+ recCtx=new (window.AudioContext||window.webkitAudioContext)();
|
|
|
248
|
+ const dest=recCtx.createMediaStreamDestination();
|
|
|
249
|
+ if(remote.getAudioTracks().length){ try{recCtx.createMediaStreamSource(new MediaStream(remote.getAudioTracks())).connect(dest);}catch(_){} }
|
|
|
250
|
+ if(window.__mic&&window.__mic.getAudioTracks().length){ try{recCtx.createMediaStreamSource(new MediaStream(window.__mic.getAudioTracks())).connect(dest);}catch(_){} }
|
|
|
251
|
+ const mixed=new MediaStream();
|
|
|
252
|
+ mixed.addTrack(remote.getVideoTracks()[0]);
|
|
|
253
|
+ dest.stream.getAudioTracks().forEach(t=>mixed.addTrack(t));
|
|
|
254
|
+ let mime='video/webm;codecs=vp8,opus'; if(!(window.MediaRecorder&&MediaRecorder.isTypeSupported(mime))) mime='video/webm';
|
|
|
255
|
+ recChunks=[];
|
|
|
256
|
+ mediaRecorder=new MediaRecorder(mixed,{mimeType:mime});
|
|
|
257
|
+ mediaRecorder.ondataavailable=(e)=>{ if(e.data&&e.data.size) recChunks.push(e.data); };
|
|
|
258
|
+ mediaRecorder.onstop=async()=>{
|
|
|
259
|
+ try{ const blob=new Blob(recChunks,{type:'video/webm'}); if(blob.size) await fetch('/api/recording?sessionId='+encodeURIComponent(sessionId),{method:'POST',headers:{'Content-Type':'video/webm'},body:blob}); }catch(_){}
|
|
|
260
|
+ try{recCtx&&recCtx.close();}catch(_){} recCtx=null;
|
|
|
261
|
+ };
|
|
|
262
|
+ mediaRecorder.start(1000);
|
|
|
263
|
+ startTranscription();
|
|
|
264
|
+ recBtnUpdate(true);
|
|
|
265
|
+ showRecTimer(true);
|
|
|
266
|
+ try{ws.send(JSON.stringify({type:'recording',sessionId,on:true}));}catch(_){}
|
|
|
267
|
+ }catch(e){ alert('Recording could not start on this browser.'); }
|
|
|
268
|
+}
|
|
|
269
|
+function stopRecording(){
|
|
|
270
|
+ if(mediaRecorder&&mediaRecorder.state!=='inactive'){ try{mediaRecorder.stop();}catch(_){} }
|
|
|
271
|
+ stopTranscription();
|
|
|
272
|
+ uploadTranscript();
|
|
|
273
|
+ recBtnUpdate(false);
|
|
|
274
|
+ showRecTimer(false);
|
|
|
275
|
+ try{ws.send(JSON.stringify({type:'recording',sessionId,on:false}));}catch(_){}
|
|
|
276
|
+}
|
|
183
|
277
|
function _btn(id,svg,label,bg){const b=document.createElement('button');b.id=id;b.innerHTML='<span style="display:inline-flex">'+svg+'</span><span>'+label+'</span>';b.style.cssText='display:inline-flex;align-items:center;gap:7px;border:none;border-radius:12px;padding:.62rem 1rem;font-weight:600;font-size:.9rem;cursor:pointer;color:#fff;background:'+bg+';transition:background .15s,transform .08s';b.onmouseenter=()=>b.style.transform='translateY(-1px)';b.onmouseleave=()=>b.style.transform='none';return b;}
|
|
184
|
278
|
function buildBar(){
|
|
185
|
279
|
if(document.getElementById('sessionBar'))return;
|
|
|
@@ -187,11 +281,13 @@ function buildBar(){
|
|
187
|
281
|
bar.style.cssText='position:fixed;left:50%;bottom:18px;transform:translateX(-50%);z-index:2147483000;display:flex;gap:10px;align-items:center;background:rgba(15,23,42,.94);padding:8px 12px;border-radius:16px;box-shadow:0 10px 28px rgba(0,0,0,.35)';
|
|
188
|
282
|
const mic=_btn('micBtn',SVG_MIC,'Mic','#2563eb');
|
|
189
|
283
|
const chat=_btn('chatBtn',SVG_CHAT,'Chat','#475569');
|
|
|
284
|
+ const rec=_btn('recBtn',SVG_REC,'','#0ea5e9'); rec.title='Record'; rec.querySelectorAll('span').forEach((s,i)=>{ if(i>0) s.remove(); });
|
|
190
|
285
|
const end=_btn('endBtn2',SVG_END,'End','#dc2626');
|
|
191
|
|
- bar.appendChild(mic);bar.appendChild(chat);bar.appendChild(end);
|
|
|
286
|
+ bar.appendChild(mic);bar.appendChild(chat);bar.appendChild(rec);bar.appendChild(end);
|
|
192
|
287
|
document.body.appendChild(bar);
|
|
193
|
288
|
mic.onclick=()=>{const m=window.__mic;if(!m)return;const t=m.getAudioTracks()[0];if(!t)return;t.enabled=!t.enabled;mic.innerHTML='<span style="display:inline-flex">'+(t.enabled?SVG_MIC:SVG_MICOFF)+'</span><span>'+(t.enabled?'Mic':'Muted')+'</span>';mic.style.background=t.enabled?'#2563eb':'#6b7280';};
|
|
194
|
289
|
chat.onclick=toggleChat;
|
|
|
290
|
+ rec.onclick=()=>{ if(mediaRecorder&&mediaRecorder.state==='recording') stopRecording(); else startRecording(); };
|
|
195
|
291
|
end.onclick=()=>{ try{ws.send(JSON.stringify({type:'end-session',sessionId,reason:'agent-ended'}));}catch(_){} };
|
|
196
|
292
|
buildChatPanel();
|
|
197
|
293
|
document.addEventListener('pointerdown',ensureAudio,{once:true});
|
|
|
@@ -216,19 +312,23 @@ let __ac=null;
|
|
216
|
312
|
function ensureAudio(){try{__ac=__ac||new (window.AudioContext||window.webkitAudioContext)();if(__ac.state==='suspended')__ac.resume();}catch(_){}}
|
|
217
|
313
|
function beep(){ensureAudio();if(!__ac)return;try{const o=__ac.createOscillator(),g=__ac.createGain();o.type='sine';o.connect(g);g.connect(__ac.destination);const t0=__ac.currentTime;o.frequency.setValueAtTime(880,t0);o.frequency.setValueAtTime(660,t0+0.09);g.gain.setValueAtTime(0.0001,t0);g.gain.exponentialRampToValueAtTime(0.12,t0+0.02);g.gain.exponentialRampToValueAtTime(0.0001,t0+0.22);o.start(t0);o.stop(t0+0.24);}catch(_){}}
|
|
218
|
314
|
try{['pointerdown','keydown','touchstart','click'].forEach(ev=>document.addEventListener(ev,ensureAudio,{passive:true}));}catch(_){}
|
|
219
|
|
-function sendChat(){const i=document.getElementById('chatInput');if(!i)return;const t=i.value.trim();if(!t)return;if(chatChannel&&chatChannel.readyState==='open'){chatChannel.send(JSON.stringify({name:(me&&(me.name||me.email))||'Support agent',text:t}));}addChat({from:'__self',name:'You',text:t});i.value='';}
|
|
|
315
|
+function sendChat(){const i=document.getElementById('chatInput');if(!i)return;const t=i.value.trim();if(!t)return;if(chatChannel&&chatChannel.readyState==='open'){chatChannel.send(JSON.stringify({name:(me&&(me.name||me.email))||'Support agent',text:t}));}addChat({from:'__self',name:'You',text:t});if(recogActive)addLine('agent',(me&&(me.name||me.email))||'Agent',t,true);i.value='';}
|
|
220
|
316
|
function removeSessionUI(){['sessionBar','chatPanel','remoteAudio','muteBtn','msgToast'].forEach(id=>{const e=document.getElementById(id);if(e)e.remove();});}
|
|
221
|
317
|
|
|
222
|
318
|
async function setupPeer(){
|
|
223
|
319
|
await ensureIce();
|
|
224
|
320
|
pc=new RTCPeerConnection(ICE);
|
|
225
|
321
|
inputChannel=pc.createDataChannel('input',{ordered:true});
|
|
226
|
|
- pc.ondatachannel=(ev)=>{ if(ev.channel.label==='chat'){ chatChannel=ev.channel; chatChannel.onmessage=(e)=>{try{addChat(JSON.parse(e.data));}catch(_){}}; } };
|
|
|
322
|
+ pc.ondatachannel=(ev)=>{ if(ev.channel.label==='chat'){ chatChannel=ev.channel; chatChannel.onmessage=(e)=>{try{const mm=JSON.parse(e.data);addChat(mm);if(recogActive)addLine('customer',mm.name||'Customer',mm.text,true);}catch(_){}}; } };
|
|
227
|
323
|
pc.ontrack=(ev)=>{
|
|
228
|
324
|
if(ev.track.kind==='audio'){ let a=document.getElementById('remoteAudio'); if(!a){a=document.createElement('audio');a.id='remoteAudio';a.autoplay=true;document.body.appendChild(a);} a.srcObject=ev.streams[0]; return; }
|
|
229
|
325
|
video.srcObject=ev.streams[0]; wrap.style.display='none'; topbar.style.display='none'; video.style.display='block'; video.focus(); buildBar();
|
|
230
|
326
|
};
|
|
231
|
327
|
pc.onicecandidate=(ev)=>{if(ev.candidate)ws.send(JSON.stringify({type:'ice-candidate',sessionId,candidate:ev.candidate}));};
|
|
|
328
|
+ pc.onconnectionstatechange=()=>{ if(!pc)return; const s=pc.connectionState;
|
|
|
329
|
+ if(s==='failed'){ renderEnded('The connection was lost. Ask the customer to refresh their page for a new code.'); }
|
|
|
330
|
+ else if(s==='disconnected'){ clearTimeout(pc._dt); pc._dt=setTimeout(()=>{ if(pc&&(pc.connectionState==='disconnected'||pc.connectionState==='failed')) renderEnded('The connection was lost. Ask the customer to refresh their page for a new code.'); },8000); }
|
|
|
331
|
+ else if(s==='connected'){ clearTimeout(pc._dt); } };
|
|
232
|
332
|
}
|
|
233
|
333
|
const send=(o)=>{if(inputChannel&&inputChannel.readyState==='open')inputChannel.send(JSON.stringify(o));};
|
|
234
|
334
|
const rel=(e)=>{const r=video.getBoundingClientRect();return{x:(e.clientX-r.left)/r.width,y:(e.clientY-r.top)/r.height};};
|