diff --git a/zebra-report/index.html b/zebra-report/index.html
index 88ae7f7..8cc7d5e 100644
--- a/zebra-report/index.html
+++ b/zebra-report/index.html
@@ -394,20 +394,32 @@ const PBKDF2_ITER = 600000;
const LOOPBACK = new URLSearchParams(location.search).has('loopback');
-const RTC_CONFIG = {
- iceServers: [
- /* our STUN/TURN on proxy.uncloseai.com — coturn deployed via
- * git.unturf.com/engineering/unturf/proxy.unturf.com.
- * credentials are public by design (baked into a static page);
- * rate-limited via coturn quotas, not via secrecy. */
- { urls: ['stun:proxy.uncloseai.com:3478', 'stun:stun.l.google.com:19302'] },
- {
- urls: 'turn:proxy.uncloseai.com:3478',
- username: 'zebra',
- credential: '7a4a2b1c8d9e6f5a'
- }
- ]
+/* STUN is free + needs no auth; TURN credentials are fetched fresh per
+ * connection from /turn-cred (time-limited HMAC, so each client gets its own
+ * coturn quota instead of sharing one static login). rtcConfig starts STUN-only
+ * and is upgraded with TURN once the creds arrive — if the fetch fails we still
+ * connect directly / via STUN, just without the relay fallback. */
+const TURN_CRED_URL = new URLSearchParams(location.search).get('turncred')
+ || 'https://cors-proxy.uncloseai.com/turn-cred';
+let rtcConfig = {
+ iceServers: [{ urls: ['stun:proxy.uncloseai.com:3478', 'stun:stun.l.google.com:19302'] }]
};
+async function refreshTurnCred() {
+ try {
+ const c = await (await fetch(TURN_CRED_URL, { cache: 'no-store' })).json();
+ if (c && c.credential && c.uris) {
+ rtcConfig = {
+ iceServers: [
+ { urls: c.stun || ['stun:proxy.uncloseai.com:3478'] },
+ { urls: c.uris, username: c.username, credential: c.credential }
+ ]
+ };
+ logLine('sys', 'TURN credentials fetched (relay fallback available)');
+ }
+ } catch (e) {
+ logLine('sys', 'no TURN credentials — direct/STUN only (' + e.message + ')');
+ }
+}
/* ============================================================== *
* tiny utils *
@@ -1132,7 +1144,7 @@ const rtcStatus = $('rtc-status');
function ensurePC() {
if (pc) return pc;
if (!carrierOn) throw new Error('start carrier first');
- pc = new RTCPeerConnection(RTC_CONFIG);
+ pc = new RTCPeerConnection(rtcConfig);
for (const tr of outboundStream.getTracks()) pc.addTrack(tr, outboundStream);
/* Force a memoryless codec (G.711 PCMU/PCMA, else G.722) ahead of Opus.
* Opus is a perceptual codec that re-quantizes in 20ms frames and smears the
@@ -1338,6 +1350,7 @@ async function connectRelay() {
logLine('sys', 'carrier auto-started for relay connect');
} catch (e) { relaySet('carrier failed: ' + e.message, 'err'); return; }
}
+ await refreshTurnCred(); /* fetch fresh ephemeral TURN creds before the PC */
try {
relayKey = await deriveSignalKey(code);
const room = await deriveSignalRoom(code);