|
|
@@ -71,7 +71,10 @@
|
|
71
|
71
|
</div>
|
|
72
|
72
|
<script>
|
|
73
|
73
|
let ICE={iceServers:[{urls:'stun:stun.l.google.com:19302'}]};
|
|
74
|
|
-try{fetch('/api/ice').then(r=>r.ok?r.json():null).then(c=>{if(c&&c.iceServers)ICE=c;}).catch(()=>{});}catch(_){}
|
|
|
74
|
+let SHARER_NAME='Customer';
|
|
|
75
|
+try{fetch('/api/me').then(r=>r.ok?r.json():null).then(m=>{if(m&&(m.name||m.email))SHARER_NAME=m.name||m.email;}).catch(()=>{});}catch(_){}
|
|
|
76
|
+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(_){}
|
|
|
77
|
+async function ensureIce(){try{await __icePromise;}catch(_){}return ICE;}
|
|
75
|
78
|
function pEsc(s){return String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));}
|
|
76
|
79
|
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>';}
|
|
77
|
80
|
function wireProfile(){const btn=document.getElementById('pbtn'),menu=document.getElementById('pmenu');if(!btn)return;btn.onclick=(e)=>{e.stopPropagation();menu.classList.toggle('open');};document.addEventListener('click',()=>menu.classList.remove('open'));const lo=document.getElementById('plogout');if(lo)lo.onclick=async()=>{try{await fetch('/api/logout',{method:'POST'});}catch(_){}location.href='/';};}
|
|
|
@@ -101,7 +104,7 @@ ws.onmessage=async(e)=>{const m=JSON.parse(e.data);switch(m.type){
|
|
101
|
104
|
case 'session-ended': endShareSession('Your support agent ended the session. Tap below for a new code if you still need help.'); break;
|
|
102
|
105
|
case 'error': setStatus(m.message,''); break;
|
|
103
|
106
|
}};
|
|
104
|
|
-ws.onclose=()=>{ if(document.getElementById('sessionBar')||localStream){ endShareSession('The connection was closed.'); } else { setStatus('Connection closed. Refresh the page to start again.'); } };
|
|
|
107
|
+ws.onclose=()=>{ if(document.getElementById('sessionBar')||localStream){ /* keep the live session: media flows independently of signaling. a real end is detected via pc 'failed' or an explicit session-ended message. */ } else { setStatus('Connection closed. Refresh the page to start again.'); } };
|
|
105
|
108
|
function onAgentConnected(m){
|
|
106
|
109
|
const cw=document.querySelector('.codewrap');
|
|
107
|
110
|
if(cw) cw.style.display='none';
|
|
|
@@ -119,16 +122,24 @@ function showConsent(m){
|
|
119
|
122
|
document.getElementById('d').onclick=()=>{consentBox.innerHTML='';document.removeEventListener('keydown',onKey);ws.send(JSON.stringify({type:'consent',sessionId:m.sessionId,granted:false}));setStatus('Connection declined. Refresh this page if you need a new code.');};
|
|
120
|
123
|
}
|
|
121
|
124
|
async function startStreaming(){
|
|
122
|
|
- setStatus('In the popup: click the screen preview so it is selected, then press Share.','on');
|
|
123
|
|
- try{ localStream=await navigator.mediaDevices.getDisplayMedia({video:{displaySurface:'monitor',frameRate:{ideal:30}},audio:false,monitorTypeSurfaces:'include'}); }
|
|
124
|
|
- catch(err){ try{ws.send(JSON.stringify({type:'end-session',sessionId,reason:'share-cancelled'}));}catch(e){} setStatus('Screen share was cancelled. Refresh the page to try again.'); return; }
|
|
|
125
|
+ // Make sure TURN/ICE is loaded before building the connection (needed for mobile/cellular relay).
|
|
|
126
|
+ await ensureIce();
|
|
|
127
|
+ // Ask for the microphone FIRST. On Android the screen capture must be the LAST
|
|
|
128
|
+ // capture started, otherwise the mic permission prompt interrupts it and the share ends.
|
|
|
129
|
+ let mic=null;
|
|
|
130
|
+ try{ mic=await navigator.mediaDevices.getUserMedia({audio:true}); }catch(e){ mic=null; }
|
|
|
131
|
+ setStatus('In the popup: choose your screen, then tap Share / Start.','on');
|
|
|
132
|
+ try{ localStream=await navigator.mediaDevices.getDisplayMedia({video:{frameRate:{ideal:30}},audio:false}); }
|
|
|
133
|
+ catch(err){ if(mic){try{mic.getTracks().forEach(t=>t.stop());}catch(_){}} try{ws.send(JSON.stringify({type:'end-session',sessionId,reason:'share-cancelled'}));}catch(e){} setStatus('Screen share was cancelled. Refresh the page to try again.'); return; }
|
|
|
134
|
+ if(mic){ window.__mic=mic; try{mic.getAudioTracks().forEach(t=>localStream.addTrack(t));}catch(_){} }
|
|
125
|
135
|
indicator.classList.add('show'); setStatus('You are now sharing your screen with your agent.','on');
|
|
126
|
136
|
pc=new RTCPeerConnection(ICE);
|
|
127
|
|
- try{ const mic=await navigator.mediaDevices.getUserMedia({audio:true}); window.__mic=mic; mic.getAudioTracks().forEach(t=>localStream.addTrack(t)); buildBar(); }catch(e){ buildBar(); }
|
|
|
137
|
+ buildBar();
|
|
128
|
138
|
localStream.getTracks().forEach(t=>pc.addTrack(t,localStream));
|
|
129
|
139
|
pc.ondatachannel=(ev)=>{ev.channel.onmessage=()=>{};};
|
|
130
|
140
|
pc.ontrack=(ev)=>{ 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]; } };
|
|
131
|
141
|
pc.onicecandidate=(ev)=>{if(ev.candidate)ws.send(JSON.stringify({type:'ice-candidate',sessionId,candidate:ev.candidate}));};
|
|
|
142
|
+ pc.onconnectionstatechange=()=>{ if(pc&&pc.connectionState==='failed'){ endShareSession('The connection was lost. Tap below for a new code if you still need help.'); } };
|
|
132
|
143
|
chatChannel=pc.createDataChannel('chat',{ordered:true});
|
|
133
|
144
|
chatChannel.onmessage=(e)=>{try{addChat(JSON.parse(e.data));}catch(_){}};
|
|
134
|
145
|
const offer=await pc.createOffer(); await pc.setLocalDescription(offer);
|
|
|
@@ -187,7 +198,7 @@ let __ac=null;
|
|
187
|
198
|
function ensureAudio(){try{__ac=__ac||new (window.AudioContext||window.webkitAudioContext)();if(__ac.state==='suspended')__ac.resume();}catch(_){}}
|
|
188
|
199
|
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(_){}}
|
|
189
|
200
|
try{['pointerdown','keydown','touchstart','click'].forEach(ev=>document.addEventListener(ev,ensureAudio,{passive:true}));}catch(_){}
|
|
190
|
|
-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:'Customer',text:t}));}addChat({from:'__self',name:'You',text:t});i.value='';}
|
|
|
201
|
+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:SHARER_NAME,text:t}));}addChat({from:'__self',name:'You',text:t});i.value='';}
|
|
191
|
202
|
function removeSessionUI(){['sessionBar','chatPanel','remoteAudio','muteBtn','msgToast'].forEach(id=>{const e=document.getElementById(id);if(e)e.remove();});}
|
|
192
|
203
|
|
|
193
|
204
|
function esc(s){return String(s).replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));}
|