feat(turn): self-hosted coturn support + time-limited creds + failure UX

- /api/ice: when TURN_SECRET is set, mint short-lived HMAC credentials
  (coturn use-auth-secret) so no permanent password is exposed and the relay
  can't be abused. Static TURN_USERNAME/CREDENTIAL still supported.
- share.html: connection watchdog + clear "couldn't connect on this network"
  message instead of a blank screen when no path can be established.
- deploy/coturn: ready-to-run turnserver.conf + docker-compose + README for
  hosting our own TURN on a VM we own (flat cost, no per-GB billing).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Этот коммит содержится в:
2026-06-16 14:36:05 +05:30
родитель 6ac280f178
Коммит 54b74d5db1
5 изменённых файлов: 121 добавлений и 8 удалений
+5 -1
Просмотреть файл
@@ -168,11 +168,15 @@ async function startStreaming(){
pc.ondatachannel=(ev)=>{ev.channel.onmessage=()=>{};};
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]; } };
pc.onicecandidate=(ev)=>{if(ev.candidate)ws.send(JSON.stringify({type:'ice-candidate',sessionId,candidate:ev.candidate}));};
pc.onconnectionstatechange=()=>{ if(pc&&pc.connectionState==='failed'){ try{ws.send(JSON.stringify({type:'end-session',sessionId,reason:'customer-ended'}));}catch(_){} endShareSession('The connection was lost. Tap below for a new code if you still need help.'); } };
pc.onconnectionstatechange=()=>{ if(!pc) return; if(pc.connectionState==='connected'){ clearTimeout(window.__connWatch); } if(pc.connectionState==='failed'){ clearTimeout(window.__connWatch); try{ws.send(JSON.stringify({type:'end-session',sessionId,reason:'customer-ended'}));}catch(_){} endShareSession("Couldn't connect on this network — it may be blocking screen sharing. Try a different network (e.g. mobile data / hotspot), then tap below for a new code."); } };
chatChannel=pc.createDataChannel('chat',{ordered:true});
chatChannel.onmessage=(e)=>{try{addChat(JSON.parse(e.data));}catch(_){}};
const offer=await pc.createOffer(); await pc.setLocalDescription(offer);
ws.send(JSON.stringify({type:'offer',sessionId,sdp:pc.localDescription}));
// Watchdog: if we can't establish the peer connection in time, show a clear reason
// instead of a blank screen (covers networks with no usable path even via TURN).
clearTimeout(window.__connWatch);
window.__connWatch=setTimeout(()=>{ if(pc && pc.connectionState!=='connected' && !sessionOver){ try{ws.send(JSON.stringify({type:'end-session',sessionId,reason:'customer-ended'}));}catch(_){} endShareSession("Couldn't connect on this network — it may be blocking screen sharing. Try a different network (e.g. mobile data / hotspot), then tap below for a new code."); } }, 20000);
localStream.getVideoTracks()[0].onended=()=>{ws.send(JSON.stringify({type:'end-session',sessionId,reason:'customer-ended'}));teardown();};
}
const SR = window.SpeechRecognition || window.webkitSpeechRecognition;