@crestapps/ai-chat-ui 2.0.0-preview.170 → 2.0.0-preview.173
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
|
|
@@ -1378,10 +1385,12 @@
|
|
|
1378
1385
|
console.info('The realtime voice session ended (' + reason + ').');
|
|
1379
1386
|
}
|
|
1380
1387
|
|
|
1381
|
-
// An idle close is deliberate and recoverable, so say so and offer to pick the conversation
|
|
1382
|
-
// rather than leaving the user staring at a button that silently stopped working.
|
|
1388
|
+
// An idle or duration close is deliberate and recoverable, so say so and offer to pick the conversation
|
|
1389
|
+
// back up rather than leaving the user staring at a button that silently stopped working.
|
|
1383
1390
|
if (reason === 'idle') {
|
|
1384
1391
|
showRealtimeStatus(localize('endedIdle', 'Voice paused after a period of silence.'), true);
|
|
1392
|
+
} else if (reason === 'max_duration') {
|
|
1393
|
+
showRealtimeStatus(localize('endedMaxDuration', 'Voice paused — this session reached its time limit. You can start another.'), true);
|
|
1385
1394
|
} else if (reason === 'disconnected') {
|
|
1386
1395
|
showRealtimeStatus(localize('endedDisconnected', 'Voice session ended — the connection dropped.'), true);
|
|
1387
1396
|
} else if (reason === 'error') {
|
|
@@ -1441,28 +1450,63 @@
|
|
|
1441
1450
|
if (webRtcIceServers) {
|
|
1442
1451
|
return Promise.resolve(webRtcIceServers);
|
|
1443
1452
|
}
|
|
1453
|
+
if (realtimeAttemptIceServers) {
|
|
1454
|
+
return Promise.resolve(realtimeAttemptIceServers);
|
|
1455
|
+
}
|
|
1444
1456
|
if (!connection || typeof connection.invoke !== 'function') {
|
|
1445
1457
|
return Promise.resolve(DEFAULT_ICE_SERVERS);
|
|
1446
1458
|
}
|
|
1447
1459
|
return connection.invoke('GetRealtimeIceServers').then(function (servers) {
|
|
1448
|
-
|
|
1460
|
+
realtimeAttemptIceServers = Array.isArray(servers) && servers.length ? servers : DEFAULT_ICE_SERVERS;
|
|
1461
|
+
return realtimeAttemptIceServers;
|
|
1449
1462
|
})["catch"](function (err) {
|
|
1450
1463
|
if (window.console && console.warn) {
|
|
1451
1464
|
console.warn('Could not resolve the realtime ICE servers; using the default STUN server.', err);
|
|
1452
1465
|
}
|
|
1453
|
-
|
|
1466
|
+
realtimeAttemptIceServers = DEFAULT_ICE_SERVERS;
|
|
1467
|
+
return realtimeAttemptIceServers;
|
|
1454
1468
|
});
|
|
1455
1469
|
}
|
|
1456
|
-
|
|
1470
|
+
|
|
1471
|
+
// Identifies an ICE configuration by its server URLs, deliberately ignoring usernames and credentials:
|
|
1472
|
+
// ephemeral TURN credentials (Cloudflare, coturn HMAC) are reissued for every session, so including them
|
|
1473
|
+
// would change the fingerprint on each attempt and make the remembered failure worthless. Adding, removing
|
|
1474
|
+
// or repointing a STUN/TURN server does change it — which is the case that must invalidate the memory.
|
|
1475
|
+
function iceServersFingerprint(servers) {
|
|
1476
|
+
var urls = [];
|
|
1457
1477
|
try {
|
|
1458
|
-
|
|
1478
|
+
(servers || []).forEach(function (server) {
|
|
1479
|
+
var list = server && server.urls;
|
|
1480
|
+
if (typeof list === 'string') {
|
|
1481
|
+
urls.push(list);
|
|
1482
|
+
} else if (Array.isArray(list)) {
|
|
1483
|
+
list.forEach(function (url) {
|
|
1484
|
+
if (url) {
|
|
1485
|
+
urls.push(url);
|
|
1486
|
+
}
|
|
1487
|
+
});
|
|
1488
|
+
}
|
|
1489
|
+
});
|
|
1490
|
+
} catch (err) {
|
|
1491
|
+
return 'none';
|
|
1492
|
+
}
|
|
1493
|
+
return urls.length ? urls.sort().join('|') : 'none';
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
// A remembered failure only applies to the ICE configuration that produced it. Anything else strands a
|
|
1497
|
+
// deployment that has since been given a TURN server on the WebSocket transport until every open tab is
|
|
1498
|
+
// closed, with no way for the user to tell why. Values written by older builds ('1') match no fingerprint,
|
|
1499
|
+
// so upgrading clears the flag on its own.
|
|
1500
|
+
function isWebRtcKnownBlocked(servers) {
|
|
1501
|
+
try {
|
|
1502
|
+
return window.sessionStorage.getItem(REALTIME_WEBRTC_BLOCKED_KEY) === iceServersFingerprint(servers);
|
|
1459
1503
|
} catch (err) {
|
|
1460
1504
|
return false;
|
|
1461
1505
|
}
|
|
1462
1506
|
}
|
|
1463
1507
|
function rememberWebRtcBlocked() {
|
|
1464
1508
|
try {
|
|
1465
|
-
window.sessionStorage.setItem(REALTIME_WEBRTC_BLOCKED_KEY,
|
|
1509
|
+
window.sessionStorage.setItem(REALTIME_WEBRTC_BLOCKED_KEY, iceServersFingerprint(realtimeAttemptIceServers || webRtcIceServers));
|
|
1466
1510
|
} catch (err) {}
|
|
1467
1511
|
}
|
|
1468
1512
|
|
|
@@ -1503,6 +1547,8 @@
|
|
|
1503
1547
|
return;
|
|
1504
1548
|
}
|
|
1505
1549
|
applyRealtimeAudioPrefs(loadRealtimeAudioPrefs());
|
|
1550
|
+
realtimeAttemptIceServers = null;
|
|
1551
|
+
var attempt = ++realtimeAttemptToken;
|
|
1506
1552
|
realtimeFellBack = false;
|
|
1507
1553
|
realtimeSessionReady = false;
|
|
1508
1554
|
realtimeEndedNotice = null;
|
|
@@ -1510,20 +1556,34 @@
|
|
|
1510
1556
|
bindRealtimeLifecycleHandlers();
|
|
1511
1557
|
setRealtimeState('requesting-mic');
|
|
1512
1558
|
|
|
1559
|
+
// Starting on WebSocket without even attempting WebRTC is a deployment fact worth stating once. The
|
|
1560
|
+
// console is otherwise indistinguishable from a healthy WebRTC session, so "no warning" gets read as
|
|
1561
|
+
// "WebRTC is working" when it can equally mean the server never offered the transport at all.
|
|
1562
|
+
if (!webRtcEnabled) {
|
|
1563
|
+
logWebSocketTransportReason('the server did not advertise the WebRTC transport');
|
|
1564
|
+
startRealtimeWebSocketConversation();
|
|
1565
|
+
return;
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1513
1568
|
// Prefer the WebRTC transport when the server advertises it: the browser's echo canceller references
|
|
1514
1569
|
// the assistant's media track, so the model can ignore its own voice with the mic open (open rooms).
|
|
1515
1570
|
// If the peer cannot connect (blocked UDP, no TURN, unsupported), we fall back to WebSocket at connect
|
|
1516
1571
|
// time — see fallbackToWebSocket. The decision is made once, before the session starts.
|
|
1517
|
-
|
|
1572
|
+
//
|
|
1573
|
+
// Resolving the ICE servers before choosing a transport costs no extra round-trip — the peer reuses
|
|
1574
|
+
// realtimeAttemptIceServers — and it is what lets a remembered failure be scoped to the configuration
|
|
1575
|
+
// that actually caused it.
|
|
1576
|
+
resolveIceServers().then(function (servers) {
|
|
1577
|
+
if (attempt !== realtimeAttemptToken || !isRealtimeMode || isRealtimeActive) {
|
|
1578
|
+
return;
|
|
1579
|
+
}
|
|
1580
|
+
if (isWebRtcKnownBlocked(servers)) {
|
|
1581
|
+
logWebSocketTransportReason('a WebRTC attempt already failed earlier in this browser session with the same ICE configuration');
|
|
1582
|
+
startRealtimeWebSocketConversation();
|
|
1583
|
+
return;
|
|
1584
|
+
}
|
|
1518
1585
|
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();
|
|
1586
|
+
});
|
|
1527
1587
|
}
|
|
1528
1588
|
function startRealtimeWebSocketConversation() {
|
|
1529
1589
|
if (webRtcEnabled) {
|