@anam-ai/js-sdk 4.17.0 → 4.18.0
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/main/lib/ClientMetrics.d.ts +10 -2
- package/dist/main/lib/ClientMetrics.d.ts.map +1 -1
- package/dist/main/lib/ClientMetrics.js +46 -31
- package/dist/main/lib/ClientMetrics.js.map +1 -1
- package/dist/main/lib/ConnectionMilestones.d.ts +2 -2
- package/dist/main/lib/ConnectionMilestones.d.ts.map +1 -1
- package/dist/main/lib/ConnectionMilestones.js +16 -5
- package/dist/main/lib/ConnectionMilestones.js.map +1 -1
- package/dist/main/lib/constants.js +1 -1
- package/dist/main/modules/SignallingClient.d.ts +23 -0
- package/dist/main/modules/SignallingClient.d.ts.map +1 -1
- package/dist/main/modules/SignallingClient.js +107 -14
- package/dist/main/modules/SignallingClient.js.map +1 -1
- package/dist/main/modules/StreamingClient.d.ts +36 -0
- package/dist/main/modules/StreamingClient.d.ts.map +1 -1
- package/dist/main/modules/StreamingClient.js +363 -25
- package/dist/main/modules/StreamingClient.js.map +1 -1
- package/dist/module/lib/ClientMetrics.d.ts +10 -2
- package/dist/module/lib/ClientMetrics.d.ts.map +1 -1
- package/dist/module/lib/ClientMetrics.js +43 -29
- package/dist/module/lib/ClientMetrics.js.map +1 -1
- package/dist/module/lib/ConnectionMilestones.d.ts +2 -2
- package/dist/module/lib/ConnectionMilestones.d.ts.map +1 -1
- package/dist/module/lib/ConnectionMilestones.js +17 -6
- package/dist/module/lib/ConnectionMilestones.js.map +1 -1
- package/dist/module/lib/constants.js +1 -1
- package/dist/module/modules/SignallingClient.d.ts +23 -0
- package/dist/module/modules/SignallingClient.d.ts.map +1 -1
- package/dist/module/modules/SignallingClient.js +107 -14
- package/dist/module/modules/SignallingClient.js.map +1 -1
- package/dist/module/modules/StreamingClient.d.ts +36 -0
- package/dist/module/modules/StreamingClient.d.ts.map +1 -1
- package/dist/module/modules/StreamingClient.js +363 -25
- package/dist/module/modules/StreamingClient.js.map +1 -1
- package/dist/umd/anam.js +1 -1
- package/package.json +1 -1
|
@@ -14,8 +14,21 @@ export declare class StreamingClient {
|
|
|
14
14
|
private rtcConfiguration;
|
|
15
15
|
private apiGatewayConfig;
|
|
16
16
|
private peerConnection;
|
|
17
|
+
private connectionEstablishedEmitted;
|
|
18
|
+
private iceRestartInProgress;
|
|
19
|
+
private iceRestartAttempts;
|
|
20
|
+
private iceRestartAwaitedAnswer;
|
|
21
|
+
private iceRestartEpisodeStartMs;
|
|
22
|
+
private iceRestartEpisodeTrigger;
|
|
23
|
+
private iceRestartStopped;
|
|
24
|
+
private iceDisconnectedGraceTimer;
|
|
25
|
+
private iceRestartWatchdogTimer;
|
|
26
|
+
private pendingWsOpenListener;
|
|
27
|
+
private pendingWsOpenTimeout;
|
|
28
|
+
private pendingWsOpenReject;
|
|
17
29
|
private connectionReceivedAnswer;
|
|
18
30
|
private remoteIceCandidateBuffer;
|
|
31
|
+
private iceRestartCandidateBuffer;
|
|
19
32
|
private inputAudioStream;
|
|
20
33
|
private dataChannel;
|
|
21
34
|
private videoElement;
|
|
@@ -83,6 +96,29 @@ export declare class StreamingClient {
|
|
|
83
96
|
*/
|
|
84
97
|
private onIceCandidate;
|
|
85
98
|
private onIceConnectionStateChange;
|
|
99
|
+
private clearIceDisconnectedGraceTimer;
|
|
100
|
+
private clearIceRestartWatchdog;
|
|
101
|
+
private sendIceRestartMetric;
|
|
102
|
+
private isAwaitingRestartAnswer;
|
|
103
|
+
private onAnswerAccepted;
|
|
104
|
+
private clearPendingWsOpenWait;
|
|
105
|
+
private cancelIceRestart;
|
|
106
|
+
private flushIceRestartCandidateBuffer;
|
|
107
|
+
private scheduleIceRestartAfterGrace;
|
|
108
|
+
/**
|
|
109
|
+
* Resolve once the signalling WebSocket is open (it auto-reconnects on close),
|
|
110
|
+
* or reject if it is terminally closed / times out. Restart offers must never
|
|
111
|
+
* be sent on a dead socket. Timeout + listener are stored so shutdown can clear
|
|
112
|
+
* them (see clearPendingWsOpenWait).
|
|
113
|
+
*/
|
|
114
|
+
private ensureSignallingConnected;
|
|
115
|
+
/**
|
|
116
|
+
* Automatic ICE restart. Mints a new offer with fresh ICE credentials and
|
|
117
|
+
* sends it over the existing channel. Bounded retries via a watchdog; on
|
|
118
|
+
* exhaustion or terminal signalling, falls back to the WebRTC-failure path.
|
|
119
|
+
*/
|
|
120
|
+
private restartIce;
|
|
121
|
+
private onIceRestartWatchdog;
|
|
86
122
|
private onConnectionStateChange;
|
|
87
123
|
private handleWebrtcFailure;
|
|
88
124
|
private onTrackEventHandler;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StreamingClient.d.ts","sourceRoot":"","sources":["../../../src/modules/StreamingClient.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,iCAAiC,EAAE,MAAM,6BAA6B,CAAC;AAChF,OAAO,EAEL,oBAAoB,EACpB,kBAAkB,EAEnB,MAAM,YAAY,CAAC;AACpB,OAAO,EAGL,qBAAqB,EAIrB,eAAe,EAIf,sBAAsB,EAIvB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AAEvE,OAAO,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAO/D,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"StreamingClient.d.ts","sourceRoot":"","sources":["../../../src/modules/StreamingClient.ts"],"names":[],"mappings":"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"}
|
|
@@ -19,12 +19,34 @@ const ToolCallManager_1 = require("./ToolCallManager");
|
|
|
19
19
|
const SUCCESS_METRIC_POLLING_TIMEOUT_MS = 15000; // After this time we will stop polling for the first frame and consider the session a failure.
|
|
20
20
|
const STATS_COLLECTION_INTERVAL_MS = 5000;
|
|
21
21
|
const ICE_CANDIDATE_POOL_SIZE = 2; // Optimisation to speed up connection time
|
|
22
|
+
const MAX_ICE_RESTART_ATTEMPTS = 3;
|
|
23
|
+
const ICE_DISCONNECTED_GRACE_MS = 2000;
|
|
24
|
+
const ICE_RESTART_WATCHDOG_MS = 3000;
|
|
25
|
+
const ENSURE_WS_OPEN_TIMEOUT_MS = 5000;
|
|
22
26
|
class StreamingClient {
|
|
23
27
|
constructor(sessionId, options, publicEventEmitter, internalEventEmitter, toolCallManager, connectionMilestones) {
|
|
24
28
|
var _a, _b, _c, _d;
|
|
25
29
|
this.peerConnection = null;
|
|
30
|
+
this.connectionEstablishedEmitted = false;
|
|
31
|
+
this.iceRestartInProgress = false;
|
|
32
|
+
this.iceRestartAttempts = 0;
|
|
33
|
+
this.iceRestartAwaitedAnswer = false;
|
|
34
|
+
// Restart-episode telemetry (see sendIceRestartMetric). null = no episode.
|
|
35
|
+
this.iceRestartEpisodeStartMs = null;
|
|
36
|
+
this.iceRestartEpisodeTrigger = null;
|
|
37
|
+
this.iceRestartStopped = false; // set on shutdown; halts all restart activity
|
|
38
|
+
this.iceDisconnectedGraceTimer = null;
|
|
39
|
+
this.iceRestartWatchdogTimer = null;
|
|
40
|
+
this.pendingWsOpenListener = null;
|
|
41
|
+
this.pendingWsOpenTimeout = null;
|
|
42
|
+
this.pendingWsOpenReject = null;
|
|
26
43
|
this.connectionReceivedAnswer = false;
|
|
27
44
|
this.remoteIceCandidateBuffer = [];
|
|
45
|
+
// While a re-offer is being minted and sent, local candidates are buffered here
|
|
46
|
+
// so they reach the server AFTER the restart offer (which carries the new ICE
|
|
47
|
+
// credentials); otherwise the engine adds them against the old generation and
|
|
48
|
+
// drops them. null = not buffering (normal path).
|
|
49
|
+
this.iceRestartCandidateBuffer = null;
|
|
28
50
|
this.inputAudioStream = null;
|
|
29
51
|
this.dataChannel = null;
|
|
30
52
|
this.videoElement = null;
|
|
@@ -431,9 +453,30 @@ class StreamingClient {
|
|
|
431
453
|
case types_1.SignalMessageAction.ANSWER: {
|
|
432
454
|
(_a = this.connectionMilestones) === null || _a === void 0 ? void 0 : _a.record('answer_received');
|
|
433
455
|
const answer = signalMessage.payload;
|
|
434
|
-
|
|
456
|
+
if (this.peerConnection.signalingState !== 'have-local-offer') {
|
|
457
|
+
// Late answer to a superseded / rolled-back restart offer — ignore it so
|
|
458
|
+
// it cannot be applied in a stable state or against a different offer.
|
|
459
|
+
break;
|
|
460
|
+
}
|
|
461
|
+
try {
|
|
462
|
+
yield this.peerConnection.setRemoteDescription(answer);
|
|
463
|
+
}
|
|
464
|
+
catch (err) {
|
|
465
|
+
console.error('StreamingClient - setRemoteDescription(answer) failed', err);
|
|
466
|
+
if (!this.connectionEstablishedEmitted &&
|
|
467
|
+
!this.isAwaitingRestartAnswer()) {
|
|
468
|
+
// Initial-connection answer: no restart watchdog will retry, so
|
|
469
|
+
// surface the failure instead of hanging the connection. Once the
|
|
470
|
+
// session has been established, any answer is a restart answer
|
|
471
|
+
// (even if ICE recovered on its own and cleared the restart flags
|
|
472
|
+
// before it arrived) — ICE-failure handling owns recovery then.
|
|
473
|
+
this.handleWebrtcFailure(err);
|
|
474
|
+
}
|
|
475
|
+
break; // restart answers: let the ICE restart watchdog retry
|
|
476
|
+
}
|
|
435
477
|
(_b = this.connectionMilestones) === null || _b === void 0 ? void 0 : _b.record('remote_description_set');
|
|
436
478
|
this.connectionReceivedAnswer = true;
|
|
479
|
+
this.onAnswerAccepted();
|
|
437
480
|
// flush the remote buffer
|
|
438
481
|
yield this.flushRemoteIceCandidateBuffer();
|
|
439
482
|
break;
|
|
@@ -455,6 +498,8 @@ class StreamingClient {
|
|
|
455
498
|
}
|
|
456
499
|
case types_1.SignalMessageAction.END_SESSION:
|
|
457
500
|
const reason = signalMessage.payload;
|
|
501
|
+
// Server-ended session mid-restart counts as aborted; user hang-up doesn't.
|
|
502
|
+
this.sendIceRestartMetric('aborted');
|
|
458
503
|
(_d = this.connectionMilestones) === null || _d === void 0 ? void 0 : _d.publishFailure({
|
|
459
504
|
failureStage: 'server_closed_connection',
|
|
460
505
|
});
|
|
@@ -546,6 +591,11 @@ class StreamingClient {
|
|
|
546
591
|
this.firstLocalIceCandidateSent = true;
|
|
547
592
|
(_a = this.connectionMilestones) === null || _a === void 0 ? void 0 : _a.record('first_local_ice_candidate_sent', getIceCandidateMilestoneTags(event.candidate));
|
|
548
593
|
}
|
|
594
|
+
if (this.iceRestartCandidateBuffer) {
|
|
595
|
+
// Hold until the re-offer is sent (see restartIce).
|
|
596
|
+
this.iceRestartCandidateBuffer.push(event.candidate);
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
549
599
|
this.signallingClient.sendIceCandidate(event.candidate);
|
|
550
600
|
}
|
|
551
601
|
else {
|
|
@@ -553,31 +603,307 @@ class StreamingClient {
|
|
|
553
603
|
}
|
|
554
604
|
}
|
|
555
605
|
onIceConnectionStateChange() {
|
|
556
|
-
var _a, _b, _c
|
|
557
|
-
const
|
|
558
|
-
if (
|
|
606
|
+
var _a, _b, _c;
|
|
607
|
+
const state = (_a = this.peerConnection) === null || _a === void 0 ? void 0 : _a.iceConnectionState;
|
|
608
|
+
if (state) {
|
|
559
609
|
(_b = this.connectionMilestones) === null || _b === void 0 ? void 0 : _b.record('ice_connection_state_changed', {
|
|
560
|
-
iceConnectionState,
|
|
610
|
+
iceConnectionState: state,
|
|
561
611
|
});
|
|
562
612
|
}
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
this.
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
613
|
+
switch (state) {
|
|
614
|
+
case 'connected':
|
|
615
|
+
case 'completed':
|
|
616
|
+
this.clearIceDisconnectedGraceTimer();
|
|
617
|
+
this.clearIceRestartWatchdog();
|
|
618
|
+
this.iceRestartInProgress = false;
|
|
619
|
+
this.sendIceRestartMetric('recovered'); // before the attempts reset below
|
|
620
|
+
this.iceRestartAttempts = 0;
|
|
621
|
+
this.signallingClient.endIceRestartReconnect();
|
|
622
|
+
if (!this.connectionEstablishedMilestoneRecorded) {
|
|
623
|
+
this.connectionEstablishedMilestoneRecorded = true;
|
|
624
|
+
(_c = this.connectionMilestones) === null || _c === void 0 ? void 0 : _c.record('client_connection_established', {
|
|
625
|
+
iceConnectionState: state,
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
if (!this.connectionEstablishedEmitted) {
|
|
629
|
+
this.connectionEstablishedEmitted = true;
|
|
630
|
+
this.publicEventEmitter.emit(types_1.AnamEvent.CONNECTION_ESTABLISHED);
|
|
631
|
+
}
|
|
632
|
+
this.startStatsCollection(); // idempotent (guards on statsCollectionInterval)
|
|
633
|
+
break;
|
|
634
|
+
case 'disconnected':
|
|
635
|
+
this.scheduleIceRestartAfterGrace();
|
|
636
|
+
break;
|
|
637
|
+
case 'failed':
|
|
638
|
+
this.clearIceDisconnectedGraceTimer();
|
|
639
|
+
void this.restartIce();
|
|
640
|
+
break;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
clearIceDisconnectedGraceTimer() {
|
|
644
|
+
if (this.iceDisconnectedGraceTimer) {
|
|
645
|
+
clearTimeout(this.iceDisconnectedGraceTimer);
|
|
646
|
+
this.iceDisconnectedGraceTimer = null;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
clearIceRestartWatchdog() {
|
|
650
|
+
if (this.iceRestartWatchdogTimer) {
|
|
651
|
+
clearTimeout(this.iceRestartWatchdogTimer);
|
|
652
|
+
this.iceRestartWatchdogTimer = null;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
// One metric per restart episode, sent at episode end. No-op when no
|
|
656
|
+
// episode is active; deliberately silent on user shutdown mid-episode.
|
|
657
|
+
sendIceRestartMetric(outcome) {
|
|
658
|
+
var _a;
|
|
659
|
+
if (this.iceRestartEpisodeStartMs === null) {
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
const durationMs = Math.round(performance.now() - this.iceRestartEpisodeStartMs);
|
|
663
|
+
const trigger = (_a = this.iceRestartEpisodeTrigger) !== null && _a !== void 0 ? _a : 'unknown';
|
|
664
|
+
this.iceRestartEpisodeStartMs = null;
|
|
665
|
+
this.iceRestartEpisodeTrigger = null;
|
|
666
|
+
// durationMs is the value, not a tag: near-unique tag values explode
|
|
667
|
+
// InfluxDB series cardinality. Episode count = count of points.
|
|
668
|
+
(0, ClientMetrics_1.sendClientMetric)(ClientMetrics_1.ClientMetricMeasurement.CLIENT_METRIC_MEASUREMENT_ICE_RESTART, durationMs, {
|
|
669
|
+
outcome,
|
|
670
|
+
attempts: this.iceRestartAttempts,
|
|
671
|
+
trigger,
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
// True while a restart offer is outstanding (in-progress gate, awaited-answer
|
|
675
|
+
// grace cycle, or armed watchdog) — i.e. an incoming answer belongs to an ICE
|
|
676
|
+
// restart rather than the initial connection.
|
|
677
|
+
isAwaitingRestartAnswer() {
|
|
678
|
+
return (this.iceRestartInProgress ||
|
|
679
|
+
this.iceRestartAwaitedAnswer ||
|
|
680
|
+
this.iceRestartWatchdogTimer !== null);
|
|
681
|
+
}
|
|
682
|
+
onAnswerAccepted() {
|
|
683
|
+
var _a;
|
|
684
|
+
const wasAwaitingRestartAnswer = this.isAwaitingRestartAnswer();
|
|
685
|
+
this.clearIceRestartWatchdog();
|
|
686
|
+
this.iceRestartAwaitedAnswer = false;
|
|
687
|
+
if (!wasAwaitingRestartAnswer) {
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
this.iceRestartInProgress = false;
|
|
691
|
+
const state = (_a = this.peerConnection) === null || _a === void 0 ? void 0 : _a.iceConnectionState;
|
|
692
|
+
if (state === 'disconnected' || state === 'failed') {
|
|
693
|
+
this.scheduleIceRestartAfterGrace();
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
// Clears the pending ensureSignallingConnected wait (timeout + listener).
|
|
697
|
+
clearPendingWsOpenWait() {
|
|
698
|
+
if (this.pendingWsOpenTimeout) {
|
|
699
|
+
clearTimeout(this.pendingWsOpenTimeout);
|
|
700
|
+
this.pendingWsOpenTimeout = null;
|
|
701
|
+
}
|
|
702
|
+
if (this.pendingWsOpenListener) {
|
|
703
|
+
this.internalEventEmitter.removeListener(types_1.InternalEvent.WEB_SOCKET_OPEN, this.pendingWsOpenListener);
|
|
704
|
+
this.pendingWsOpenListener = null;
|
|
705
|
+
}
|
|
706
|
+
this.pendingWsOpenReject = null;
|
|
707
|
+
}
|
|
708
|
+
// Stops all restart activity: timers, pending WS wait, and the in-progress gate.
|
|
709
|
+
cancelIceRestart() {
|
|
710
|
+
this.clearIceDisconnectedGraceTimer();
|
|
711
|
+
this.clearIceRestartWatchdog();
|
|
712
|
+
const reject = this.pendingWsOpenReject;
|
|
713
|
+
this.clearPendingWsOpenWait();
|
|
714
|
+
if (reject)
|
|
715
|
+
reject(new Error('ice restart cancelled'));
|
|
716
|
+
this.iceRestartCandidateBuffer = null;
|
|
717
|
+
this.iceRestartInProgress = false;
|
|
718
|
+
// End the metric episode: a late END_SESSION after shutdown must not
|
|
719
|
+
// count the hang-up as an aborted restart.
|
|
720
|
+
this.iceRestartEpisodeStartMs = null;
|
|
721
|
+
this.iceRestartEpisodeTrigger = null;
|
|
722
|
+
this.signallingClient.endIceRestartReconnect();
|
|
723
|
+
}
|
|
724
|
+
// Send any candidates buffered while the re-offer was minted+sent, then stop
|
|
725
|
+
// buffering (see restartIce/onIceCandidate).
|
|
726
|
+
flushIceRestartCandidateBuffer() {
|
|
727
|
+
const buffered = this.iceRestartCandidateBuffer;
|
|
728
|
+
this.iceRestartCandidateBuffer = null;
|
|
729
|
+
if (!buffered)
|
|
730
|
+
return;
|
|
731
|
+
for (const candidate of buffered) {
|
|
732
|
+
this.signallingClient.sendIceCandidate(candidate);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
scheduleIceRestartAfterGrace() {
|
|
736
|
+
if (this.iceRestartStopped ||
|
|
737
|
+
this.iceRestartInProgress ||
|
|
738
|
+
this.iceDisconnectedGraceTimer) {
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
this.iceDisconnectedGraceTimer = setTimeout(() => {
|
|
742
|
+
var _a;
|
|
743
|
+
this.iceDisconnectedGraceTimer = null;
|
|
744
|
+
if (this.iceRestartStopped)
|
|
745
|
+
return;
|
|
746
|
+
const state = (_a = this.peerConnection) === null || _a === void 0 ? void 0 : _a.iceConnectionState;
|
|
747
|
+
if (state === 'disconnected' || state === 'failed') {
|
|
748
|
+
void this.restartIce();
|
|
570
749
|
}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
750
|
+
}, ICE_DISCONNECTED_GRACE_MS);
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* Resolve once the signalling WebSocket is open (it auto-reconnects on close),
|
|
754
|
+
* or reject if it is terminally closed / times out. Restart offers must never
|
|
755
|
+
* be sent on a dead socket. Timeout + listener are stored so shutdown can clear
|
|
756
|
+
* them (see clearPendingWsOpenWait).
|
|
757
|
+
*/
|
|
758
|
+
ensureSignallingConnected() {
|
|
759
|
+
if (this.signallingClient.isConnected()) {
|
|
760
|
+
return Promise.resolve();
|
|
574
761
|
}
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
failureStage: 'ice_connection',
|
|
578
|
-
iceConnectionState: this.peerConnection.iceConnectionState,
|
|
579
|
-
});
|
|
762
|
+
if (this.signallingClient.isPermanentlyClosed()) {
|
|
763
|
+
return Promise.reject(new Error('signalling permanently closed'));
|
|
580
764
|
}
|
|
765
|
+
return new Promise((resolve, reject) => {
|
|
766
|
+
this.pendingWsOpenReject = reject;
|
|
767
|
+
this.pendingWsOpenTimeout = setTimeout(() => {
|
|
768
|
+
this.clearPendingWsOpenWait();
|
|
769
|
+
reject(new Error('timed out waiting for signalling WebSocket'));
|
|
770
|
+
}, ENSURE_WS_OPEN_TIMEOUT_MS);
|
|
771
|
+
const onOpen = () => {
|
|
772
|
+
this.clearPendingWsOpenWait();
|
|
773
|
+
resolve();
|
|
774
|
+
};
|
|
775
|
+
this.pendingWsOpenListener = onOpen;
|
|
776
|
+
this.internalEventEmitter.addListener(types_1.InternalEvent.WEB_SOCKET_OPEN, onOpen);
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* Automatic ICE restart. Mints a new offer with fresh ICE credentials and
|
|
781
|
+
* sends it over the existing channel. Bounded retries via a watchdog; on
|
|
782
|
+
* exhaustion or terminal signalling, falls back to the WebRTC-failure path.
|
|
783
|
+
*/
|
|
784
|
+
restartIce() {
|
|
785
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
786
|
+
var _a, _b;
|
|
787
|
+
if (!this.peerConnection ||
|
|
788
|
+
this.iceRestartInProgress ||
|
|
789
|
+
this.iceRestartStopped) {
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
if (this.signallingClient.isPermanentlyClosed()) {
|
|
793
|
+
// Session is already ending via the signalling layer; stop spinning.
|
|
794
|
+
this.sendIceRestartMetric('aborted');
|
|
795
|
+
this.cancelIceRestart();
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
if (this.iceRestartAttempts >= MAX_ICE_RESTART_ATTEMPTS) {
|
|
799
|
+
console.error('StreamingClient - restartIce: exhausted attempts');
|
|
800
|
+
(_a = this.connectionMilestones) === null || _a === void 0 ? void 0 : _a.publishFailure({
|
|
801
|
+
failureStage: 'ice_connection',
|
|
802
|
+
iceConnectionState: (_b = this.peerConnection) === null || _b === void 0 ? void 0 : _b.iceConnectionState,
|
|
803
|
+
});
|
|
804
|
+
this.sendIceRestartMetric('exhausted');
|
|
805
|
+
this.cancelIceRestart();
|
|
806
|
+
this.handleWebrtcFailure('The connection to our servers was lost. Please try again.');
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
if (this.iceRestartAttempts === 0) {
|
|
810
|
+
this.iceRestartEpisodeStartMs = performance.now();
|
|
811
|
+
this.iceRestartEpisodeTrigger = this.peerConnection.iceConnectionState;
|
|
812
|
+
}
|
|
813
|
+
// Set the in-progress gate BEFORE any await so a concurrent trigger
|
|
814
|
+
// (watchdog retry + ICE 'failed' event) cannot start a second restart.
|
|
815
|
+
this.iceRestartInProgress = true;
|
|
816
|
+
this.iceRestartAttempts += 1;
|
|
817
|
+
try {
|
|
818
|
+
// Avoid offer glare: only start a fresh offer from a stable signalling state.
|
|
819
|
+
if (this.peerConnection.signalingState !== 'stable') {
|
|
820
|
+
yield this.peerConnection.setLocalDescription({ type: 'rollback' });
|
|
821
|
+
}
|
|
822
|
+
// On the first attempt of a restart episode, force a fresh signalling
|
|
823
|
+
// socket. A network switch can leave the old one half-open (readyState
|
|
824
|
+
// still OPEN, no close event), so the offer would be sent into a dead
|
|
825
|
+
// socket and never reach the server. Retries (attempt > 1) reuse the
|
|
826
|
+
// now-live socket so an in-flight answer isn't dropped.
|
|
827
|
+
if (this.iceRestartAttempts === 1) {
|
|
828
|
+
this.signallingClient.reconnectForIceRestart();
|
|
829
|
+
}
|
|
830
|
+
yield this.ensureSignallingConnected();
|
|
831
|
+
if (this.iceRestartStopped)
|
|
832
|
+
return;
|
|
833
|
+
// ICE may have recovered on its own while we waited for signalling.
|
|
834
|
+
const currentIceState = this.peerConnection.iceConnectionState;
|
|
835
|
+
if (currentIceState === 'connected' || currentIceState === 'completed') {
|
|
836
|
+
this.iceRestartInProgress = false;
|
|
837
|
+
// No-ops if the connected handler already closed the episode.
|
|
838
|
+
this.sendIceRestartMetric('recovered');
|
|
839
|
+
this.signallingClient.endIceRestartReconnect();
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
this.connectionReceivedAnswer = false;
|
|
843
|
+
this.remoteIceCandidateBuffer = [];
|
|
844
|
+
const offer = yield this.peerConnection.createOffer({ iceRestart: true });
|
|
845
|
+
// Buffer local candidates that gather from setLocalDescription until the
|
|
846
|
+
// re-offer is sent. The engine queues remote candidates only while it has
|
|
847
|
+
// no remote description; during a restart it still holds the OLD ICE
|
|
848
|
+
// credentials, so a candidate arriving before the re-offer is applied would
|
|
849
|
+
// be added against the old generation and dropped. WS messages are handled
|
|
850
|
+
// in order, so sending the offer first pins the new credentials server-side.
|
|
851
|
+
this.iceRestartCandidateBuffer = [];
|
|
852
|
+
yield this.peerConnection.setLocalDescription(offer);
|
|
853
|
+
if (!this.peerConnection.localDescription) {
|
|
854
|
+
throw new Error('null local description after ICE restart offer');
|
|
855
|
+
}
|
|
856
|
+
yield this.signallingClient.sendOffer(this.peerConnection.localDescription);
|
|
857
|
+
this.flushIceRestartCandidateBuffer();
|
|
858
|
+
this.iceRestartAwaitedAnswer = false;
|
|
859
|
+
this.clearIceRestartWatchdog();
|
|
860
|
+
this.iceRestartWatchdogTimer = setTimeout(() => this.onIceRestartWatchdog(), ICE_RESTART_WATCHDOG_MS);
|
|
861
|
+
}
|
|
862
|
+
catch (err) {
|
|
863
|
+
// The offer never made it out; drop candidates buffered for it (a retry
|
|
864
|
+
// mints a fresh offer and gathers again).
|
|
865
|
+
this.iceRestartCandidateBuffer = null;
|
|
866
|
+
console.error('StreamingClient - restartIce: error', err);
|
|
867
|
+
this.iceRestartInProgress = false;
|
|
868
|
+
if (this.iceRestartStopped) {
|
|
869
|
+
this.cancelIceRestart();
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
if (this.signallingClient.isPermanentlyClosed()) {
|
|
873
|
+
this.sendIceRestartMetric('aborted');
|
|
874
|
+
this.cancelIceRestart();
|
|
875
|
+
return; // signalling layer already emitted CONNECTION_CLOSED
|
|
876
|
+
}
|
|
877
|
+
this.clearIceRestartWatchdog();
|
|
878
|
+
this.iceRestartWatchdogTimer = setTimeout(() => {
|
|
879
|
+
this.iceRestartWatchdogTimer = null;
|
|
880
|
+
void this.restartIce();
|
|
881
|
+
}, ICE_RESTART_WATCHDOG_MS);
|
|
882
|
+
}
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
// Watchdog for an outstanding restart offer. Only rolls back and re-offers
|
|
886
|
+
// once the current offer is resolved, so a second offer is never minted while
|
|
887
|
+
// the first offer's answer is still in flight (the ANSWER handler could
|
|
888
|
+
// otherwise apply it against the wrong offer).
|
|
889
|
+
onIceRestartWatchdog() {
|
|
890
|
+
var _a;
|
|
891
|
+
this.iceRestartWatchdogTimer = null;
|
|
892
|
+
this.iceRestartInProgress = false;
|
|
893
|
+
if (this.iceRestartStopped)
|
|
894
|
+
return;
|
|
895
|
+
const state = (_a = this.peerConnection) === null || _a === void 0 ? void 0 : _a.iceConnectionState;
|
|
896
|
+
if (state === 'connected' || state === 'completed')
|
|
897
|
+
return;
|
|
898
|
+
if (!this.connectionReceivedAnswer && !this.iceRestartAwaitedAnswer) {
|
|
899
|
+
// Offer still unanswered: give the in-flight answer one more cycle before
|
|
900
|
+
// rolling it back and re-offering.
|
|
901
|
+
this.iceRestartAwaitedAnswer = true;
|
|
902
|
+
this.iceRestartInProgress = true;
|
|
903
|
+
this.iceRestartWatchdogTimer = setTimeout(() => this.onIceRestartWatchdog(), ICE_RESTART_WATCHDOG_MS);
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
void this.restartIce();
|
|
581
907
|
}
|
|
582
908
|
onConnectionStateChange() {
|
|
583
909
|
var _a, _b, _c, _d, _e;
|
|
@@ -587,11 +913,21 @@ class StreamingClient {
|
|
|
587
913
|
connectionState,
|
|
588
914
|
});
|
|
589
915
|
}
|
|
590
|
-
if (
|
|
591
|
-
(
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
916
|
+
if (connectionState === 'failed') {
|
|
917
|
+
const iceState = (_c = this.peerConnection) === null || _c === void 0 ? void 0 : _c.iceConnectionState;
|
|
918
|
+
const recovering = !this.iceRestartStopped &&
|
|
919
|
+
(iceState === 'disconnected' || iceState === 'failed');
|
|
920
|
+
// A recoverable ICE failure also flips the aggregate state to 'failed'.
|
|
921
|
+
// The recorder is first-call-wins, so don't finalize failure telemetry
|
|
922
|
+
// while the ICE restart is still recovering; the terminal publish comes
|
|
923
|
+
// from restartIce (exhausted) or the 'closed' branch below. A genuine
|
|
924
|
+
// non-ICE failure (e.g. DTLS with ICE still connected) still publishes.
|
|
925
|
+
if (!recovering) {
|
|
926
|
+
(_d = this.connectionMilestones) === null || _d === void 0 ? void 0 : _d.publishFailure({
|
|
927
|
+
failureStage: 'webrtc_connection',
|
|
928
|
+
connectionState,
|
|
929
|
+
});
|
|
930
|
+
}
|
|
595
931
|
}
|
|
596
932
|
if (((_e = this.peerConnection) === null || _e === void 0 ? void 0 : _e.connectionState) === 'closed') {
|
|
597
933
|
console.error('StreamingClient - onConnectionStateChange: Connection closed');
|
|
@@ -854,6 +1190,8 @@ class StreamingClient {
|
|
|
854
1190
|
shutdown() {
|
|
855
1191
|
return __awaiter(this, void 0, void 0, function* () {
|
|
856
1192
|
var _a;
|
|
1193
|
+
this.iceRestartStopped = true;
|
|
1194
|
+
this.cancelIceRestart();
|
|
857
1195
|
if (this.showPeerConnectionStatsReport) {
|
|
858
1196
|
const stats = yield ((_a = this.peerConnection) === null || _a === void 0 ? void 0 : _a.getStats());
|
|
859
1197
|
if (stats) {
|