@crestapps/ai-chat-ui 2.0.0-preview.170 → 2.0.0-preview.172
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/realtime-audio.js
CHANGED
|
@@ -762,6 +762,13 @@
|
|
|
762
762
|
// network where WebRTC cannot work pays the full connection timeout — and a second microphone prompt —
|
|
763
763
|
// at the start of every single conversation.
|
|
764
764
|
var REALTIME_WEBRTC_BLOCKED_KEY = 'coreai.realtime.webrtcBlocked';
|
|
765
|
+
// The ICE servers resolved for the conversation currently starting. Scoped to one attempt, not to the page:
|
|
766
|
+
// TURN credentials are short-lived, so every conversation resolves its own set — but within an attempt the
|
|
767
|
+
// transport decision and the peer must see the same servers, and one hub round-trip is enough.
|
|
768
|
+
var realtimeAttemptIceServers = null;
|
|
769
|
+
// Identifies the conversation currently starting, so an ICE resolution that lands after the user gave up
|
|
770
|
+
// (or started again) cannot open a session for an attempt that no longer exists.
|
|
771
|
+
var realtimeAttemptToken = 0;
|
|
765
772
|
var realtimeSawRelayCandidate = false;
|
|
766
773
|
// WebRTC half-duplex echo guard: the shared microphone gate (see createMicGate) watches both the mic and
|
|
767
774
|
// the assistant's remote audio and silences the outbound track unless the user is genuinely speaking, so
|
|
@@ -1441,28 +1448,63 @@
|
|
|
1441
1448
|
if (webRtcIceServers) {
|
|
1442
1449
|
return Promise.resolve(webRtcIceServers);
|
|
1443
1450
|
}
|
|
1451
|
+
if (realtimeAttemptIceServers) {
|
|
1452
|
+
return Promise.resolve(realtimeAttemptIceServers);
|
|
1453
|
+
}
|
|
1444
1454
|
if (!connection || typeof connection.invoke !== 'function') {
|
|
1445
1455
|
return Promise.resolve(DEFAULT_ICE_SERVERS);
|
|
1446
1456
|
}
|
|
1447
1457
|
return connection.invoke('GetRealtimeIceServers').then(function (servers) {
|
|
1448
|
-
|
|
1458
|
+
realtimeAttemptIceServers = Array.isArray(servers) && servers.length ? servers : DEFAULT_ICE_SERVERS;
|
|
1459
|
+
return realtimeAttemptIceServers;
|
|
1449
1460
|
})["catch"](function (err) {
|
|
1450
1461
|
if (window.console && console.warn) {
|
|
1451
1462
|
console.warn('Could not resolve the realtime ICE servers; using the default STUN server.', err);
|
|
1452
1463
|
}
|
|
1453
|
-
|
|
1464
|
+
realtimeAttemptIceServers = DEFAULT_ICE_SERVERS;
|
|
1465
|
+
return realtimeAttemptIceServers;
|
|
1454
1466
|
});
|
|
1455
1467
|
}
|
|
1456
|
-
|
|
1468
|
+
|
|
1469
|
+
// Identifies an ICE configuration by its server URLs, deliberately ignoring usernames and credentials:
|
|
1470
|
+
// ephemeral TURN credentials (Cloudflare, coturn HMAC) are reissued for every session, so including them
|
|
1471
|
+
// would change the fingerprint on each attempt and make the remembered failure worthless. Adding, removing
|
|
1472
|
+
// or repointing a STUN/TURN server does change it — which is the case that must invalidate the memory.
|
|
1473
|
+
function iceServersFingerprint(servers) {
|
|
1474
|
+
var urls = [];
|
|
1457
1475
|
try {
|
|
1458
|
-
|
|
1476
|
+
(servers || []).forEach(function (server) {
|
|
1477
|
+
var list = server && server.urls;
|
|
1478
|
+
if (typeof list === 'string') {
|
|
1479
|
+
urls.push(list);
|
|
1480
|
+
} else if (Array.isArray(list)) {
|
|
1481
|
+
list.forEach(function (url) {
|
|
1482
|
+
if (url) {
|
|
1483
|
+
urls.push(url);
|
|
1484
|
+
}
|
|
1485
|
+
});
|
|
1486
|
+
}
|
|
1487
|
+
});
|
|
1488
|
+
} catch (err) {
|
|
1489
|
+
return 'none';
|
|
1490
|
+
}
|
|
1491
|
+
return urls.length ? urls.sort().join('|') : 'none';
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
// A remembered failure only applies to the ICE configuration that produced it. Anything else strands a
|
|
1495
|
+
// deployment that has since been given a TURN server on the WebSocket transport until every open tab is
|
|
1496
|
+
// closed, with no way for the user to tell why. Values written by older builds ('1') match no fingerprint,
|
|
1497
|
+
// so upgrading clears the flag on its own.
|
|
1498
|
+
function isWebRtcKnownBlocked(servers) {
|
|
1499
|
+
try {
|
|
1500
|
+
return window.sessionStorage.getItem(REALTIME_WEBRTC_BLOCKED_KEY) === iceServersFingerprint(servers);
|
|
1459
1501
|
} catch (err) {
|
|
1460
1502
|
return false;
|
|
1461
1503
|
}
|
|
1462
1504
|
}
|
|
1463
1505
|
function rememberWebRtcBlocked() {
|
|
1464
1506
|
try {
|
|
1465
|
-
window.sessionStorage.setItem(REALTIME_WEBRTC_BLOCKED_KEY,
|
|
1507
|
+
window.sessionStorage.setItem(REALTIME_WEBRTC_BLOCKED_KEY, iceServersFingerprint(realtimeAttemptIceServers || webRtcIceServers));
|
|
1466
1508
|
} catch (err) {}
|
|
1467
1509
|
}
|
|
1468
1510
|
|
|
@@ -1503,6 +1545,8 @@
|
|
|
1503
1545
|
return;
|
|
1504
1546
|
}
|
|
1505
1547
|
applyRealtimeAudioPrefs(loadRealtimeAudioPrefs());
|
|
1548
|
+
realtimeAttemptIceServers = null;
|
|
1549
|
+
var attempt = ++realtimeAttemptToken;
|
|
1506
1550
|
realtimeFellBack = false;
|
|
1507
1551
|
realtimeSessionReady = false;
|
|
1508
1552
|
realtimeEndedNotice = null;
|
|
@@ -1510,20 +1554,34 @@
|
|
|
1510
1554
|
bindRealtimeLifecycleHandlers();
|
|
1511
1555
|
setRealtimeState('requesting-mic');
|
|
1512
1556
|
|
|
1557
|
+
// Starting on WebSocket without even attempting WebRTC is a deployment fact worth stating once. The
|
|
1558
|
+
// console is otherwise indistinguishable from a healthy WebRTC session, so "no warning" gets read as
|
|
1559
|
+
// "WebRTC is working" when it can equally mean the server never offered the transport at all.
|
|
1560
|
+
if (!webRtcEnabled) {
|
|
1561
|
+
logWebSocketTransportReason('the server did not advertise the WebRTC transport');
|
|
1562
|
+
startRealtimeWebSocketConversation();
|
|
1563
|
+
return;
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1513
1566
|
// Prefer the WebRTC transport when the server advertises it: the browser's echo canceller references
|
|
1514
1567
|
// the assistant's media track, so the model can ignore its own voice with the mic open (open rooms).
|
|
1515
1568
|
// If the peer cannot connect (blocked UDP, no TURN, unsupported), we fall back to WebSocket at connect
|
|
1516
1569
|
// time — see fallbackToWebSocket. The decision is made once, before the session starts.
|
|
1517
|
-
|
|
1570
|
+
//
|
|
1571
|
+
// Resolving the ICE servers before choosing a transport costs no extra round-trip — the peer reuses
|
|
1572
|
+
// realtimeAttemptIceServers — and it is what lets a remembered failure be scoped to the configuration
|
|
1573
|
+
// that actually caused it.
|
|
1574
|
+
resolveIceServers().then(function (servers) {
|
|
1575
|
+
if (attempt !== realtimeAttemptToken || !isRealtimeMode || isRealtimeActive) {
|
|
1576
|
+
return;
|
|
1577
|
+
}
|
|
1578
|
+
if (isWebRtcKnownBlocked(servers)) {
|
|
1579
|
+
logWebSocketTransportReason('a WebRTC attempt already failed earlier in this browser session with the same ICE configuration');
|
|
1580
|
+
startRealtimeWebSocketConversation();
|
|
1581
|
+
return;
|
|
1582
|
+
}
|
|
1518
1583
|
startRealtimeWebRtcConversation();
|
|
1519
|
-
|
|
1520
|
-
}
|
|
1521
|
-
|
|
1522
|
-
// Starting on WebSocket without even attempting WebRTC is a deployment fact worth stating once. The
|
|
1523
|
-
// console is otherwise indistinguishable from a healthy WebRTC session, so "no warning" gets read as
|
|
1524
|
-
// "WebRTC is working" when it can equally mean the server never offered the transport at all.
|
|
1525
|
-
logWebSocketTransportReason(webRtcEnabled ? 'a WebRTC attempt already failed earlier in this browser session' : 'the server did not advertise the WebRTC transport');
|
|
1526
|
-
startRealtimeWebSocketConversation();
|
|
1584
|
+
});
|
|
1527
1585
|
}
|
|
1528
1586
|
function startRealtimeWebSocketConversation() {
|
|
1529
1587
|
if (webRtcEnabled) {
|