@telnyx/webrtc 2.27.0-beta.5 → 2.27.0-beta.6

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.
@@ -1,6 +1,7 @@
1
1
  import type { Logger } from 'loglevel';
2
2
  import BaseMessage from './messages/BaseMessage';
3
3
  import Connection from './services/Connection';
4
+ import type { PeerFailureEvidence } from './util/interfaces/SignalingHealth';
4
5
  import { SwEvent } from './util/constants';
5
6
  import type { ITelnyxErrorEvent } from './util/errors';
6
7
  import { BroadcastParams, ILoginParams, IVertoOptions } from './util/interfaces';
@@ -36,6 +37,7 @@ export default abstract class BaseSession {
36
37
  private static readonly TOKEN_EXPIRY_WARNING_SECONDS;
37
38
  private static readonly CALL_REPORT_UPLOAD_DRAIN_TIMEOUT_MS;
38
39
  private _pendingCallReportUploads;
40
+ private _signalingHealthMonitor;
39
41
  private _executeQueue;
40
42
  private _pong;
41
43
  private registerAgent;
@@ -94,6 +96,14 @@ export default abstract class BaseSession {
94
96
  private _resetKeepAlive;
95
97
  _triggerKeepAliveTimeoutCheck(): void;
96
98
  setPingReceived(): void;
99
+ private _onSocketActivity;
100
+ hasActiveCall(): boolean;
101
+ startSignalingHealthMonitor(): void;
102
+ stopSignalingHealthMonitor(): void;
103
+ triggerIceRestart(callId: string): void;
104
+ onSignalingRequestTimeout(requestId: string, timeoutMs: number, method?: string): void;
105
+ reportPeerFailure(callId: string, evidence: PeerFailureEvidence): void;
106
+ reportNoRtp(callId: string, direction: 'inbound' | 'outbound'): void;
97
107
  static on(eventName: string, callback: any): void;
98
108
  static off(eventName: string): void;
99
109
  static uuid(): string;
@@ -3,14 +3,20 @@ export declare const setWebSocket: (websocket: typeof WebSocket) => void;
3
3
  export default class Connection {
4
4
  session: BaseSession;
5
5
  previousGatewayState: string;
6
+ lastInboundAt: number;
7
+ socketGeneration: number;
6
8
  private _wsClient;
7
9
  private _host;
8
10
  private _timers;
9
11
  private _useCanaryRtcServer;
10
12
  private _hasCanaryBeenUsed;
11
13
  private _safetyTimeoutId;
14
+ private _pendingRequestIds;
15
+ private _pendingRequestTimers;
16
+ private _pendingRequestRejecters;
12
17
  upDur: number | null;
13
18
  downDur: number | null;
19
+ static DEFAULT_REQUEST_TIMEOUT_MS: number;
14
20
  constructor(session: BaseSession);
15
21
  get connected(): boolean;
16
22
  get connecting(): boolean;
@@ -21,13 +27,14 @@ export default class Connection {
21
27
  get host(): string;
22
28
  connect(): void;
23
29
  sendRawText(request: string): void;
24
- send(bladeObj: any): Promise<any>;
30
+ send(bladeObj: any, timeoutMs?: number): Promise<any>;
25
31
  close(): void;
26
32
  private _registerSocketEvents;
27
33
  private _deregisterSocketEvents;
28
34
  private _handleCloseTimeout;
29
35
  private _clearSafetyTimeout;
30
36
  private _safetyCleanupSocket;
37
+ private _cleanupPendingRequests;
31
38
  private _unsetTimer;
32
39
  private _handleStringResponse;
33
40
  }
@@ -0,0 +1,28 @@
1
+ import type { ISignalingHealthSession, PeerFailureEvidence } from '../util/interfaces/SignalingHealth';
2
+ export default class SignalingHealthMonitor {
3
+ private readonly _session;
4
+ private _lastInboundAt;
5
+ private _lastProbeSentAt;
6
+ private _probeInFlight;
7
+ private _intervalId;
8
+ private _recoveryGeneration;
9
+ private _pendingMediaRecovery;
10
+ private static readonly CRITICAL_METHODS;
11
+ static isCriticalMethod(method: string): boolean;
12
+ constructor(_session: ISignalingHealthSession);
13
+ start(): void;
14
+ stop(): void;
15
+ get isRunning(): boolean;
16
+ get isProbeInFlight(): boolean;
17
+ onSocketActivity(): void;
18
+ private _probeIfNeeded;
19
+ onRequestTimeout(requestId: string, timeoutMs: number, method?: string): void;
20
+ onPeerFailure(callId: string, evidence: PeerFailureEvidence): void;
21
+ onNoRtp(callId: string, direction: 'inbound' | 'outbound'): void;
22
+ private _check;
23
+ private _sendProbe;
24
+ private _recoverMediaOrSignaling;
25
+ private _getSignalingHealthState;
26
+ private _triggerSignalingRecovery;
27
+ private _triggerIceRestart;
28
+ }
@@ -37,10 +37,15 @@ export declare const TELNYX_WARNING_CODES: {
37
37
  readonly PEER_CONNECTION_FAILED: 33004;
38
38
  readonly ONLY_HOST_ICE_CANDIDATES: 33005;
39
39
  readonly ANSWER_WHILE_PEER_ACTIVE: 33006;
40
+ readonly ICE_CANDIDATE_PAIR_CHANGED: 33008;
40
41
  readonly DUPLICATE_INBOUND_ANSWER: 33007;
41
42
  readonly TOKEN_EXPIRING_SOON: 34001;
42
43
  readonly SESSION_NOT_REATTACHED: 35001;
44
+ readonly SIGNALING_HEALTH_PROBE_TIMEOUT: 36001;
45
+ readonly SIGNALING_REQUEST_TIMEOUT: 36002;
46
+ readonly SIGNALING_RECOVERY_REQUIRED: 36003;
47
+ readonly MEDIA_RECOVERY_REQUIRED: 36004;
43
48
  };
44
49
  export declare const SDP_CREATE_OFFER_FAILED: 40001, SDP_CREATE_ANSWER_FAILED: 40002, SDP_SET_LOCAL_DESCRIPTION_FAILED: 40003, SDP_SET_REMOTE_DESCRIPTION_FAILED: 40004, SDP_SEND_FAILED: 40005, MEDIA_MICROPHONE_PERMISSION_DENIED: 42001, MEDIA_DEVICE_NOT_FOUND: 42002, MEDIA_GET_USER_MEDIA_FAILED: 42003, HOLD_FAILED: 44001, INVALID_CALL_PARAMETERS: 44002, BYE_SEND_FAILED: 44003, SUBSCRIBE_FAILED: 44004, PEER_CLOSED_DURING_INIT: 44005, WEBSOCKET_CONNECTION_FAILED: 45001, WEBSOCKET_ERROR: 45002, RECONNECTION_EXHAUSTED: 45003, GATEWAY_FAILED: 45004, LOGIN_FAILED: 46001, INVALID_CREDENTIALS: 46002, AUTHENTICATION_REQUIRED: 46003, ICE_RESTART_FAILED: 47001, NETWORK_OFFLINE: 48001, UNEXPECTED_ERROR: 49001;
45
- export declare const HIGH_RTT: 31001, HIGH_JITTER: 31002, HIGH_PACKET_LOSS: 31003, LOW_MOS: 31004, LOW_LOCAL_AUDIO: 31005, LOW_BYTES_RECEIVED: 32001, LOW_BYTES_SENT: 32002, ICE_CONNECTIVITY_LOST: 33001, ICE_GATHERING_TIMEOUT: 33002, ICE_GATHERING_EMPTY: 33003, PEER_CONNECTION_FAILED: 33004, ONLY_HOST_ICE_CANDIDATES: 33005, ANSWER_WHILE_PEER_ACTIVE: 33006, DUPLICATE_INBOUND_ANSWER: 33007, TOKEN_EXPIRING_SOON: 34001, SESSION_NOT_REATTACHED: 35001;
50
+ export declare const HIGH_RTT: 31001, HIGH_JITTER: 31002, HIGH_PACKET_LOSS: 31003, LOW_MOS: 31004, LOW_LOCAL_AUDIO: 31005, LOW_BYTES_RECEIVED: 32001, LOW_BYTES_SENT: 32002, ICE_CONNECTIVITY_LOST: 33001, ICE_GATHERING_TIMEOUT: 33002, ICE_GATHERING_EMPTY: 33003, PEER_CONNECTION_FAILED: 33004, ONLY_HOST_ICE_CANDIDATES: 33005, ANSWER_WHILE_PEER_ACTIVE: 33006, ICE_CANDIDATE_PAIR_CHANGED: 33008, DUPLICATE_INBOUND_ANSWER: 33007, TOKEN_EXPIRING_SOON: 34001, SESSION_NOT_REATTACHED: 35001, SIGNALING_HEALTH_PROBE_TIMEOUT: 36001, SIGNALING_REQUEST_TIMEOUT: 36002, SIGNALING_RECOVERY_REQUIRED: 36003, MEDIA_RECOVERY_REQUIRED: 36004;
46
51
  export declare const HAS_NON_HOST_ICE_CANDIDATE_REGEX: RegExp;
@@ -45,6 +45,7 @@ export declare enum SwEvent {
45
45
  SocketError = "telnyx.socket.error",
46
46
  SocketMessage = "telnyx.socket.message",
47
47
  SpeedTest = "telnyx.internal.speedtest",
48
+ SocketActivity = "telnyx.internal.socketActivity",
48
49
  Ready = "telnyx.ready",
49
50
  Error = "telnyx.error",
50
51
  Warning = "telnyx.warning",
@@ -103,6 +103,13 @@ export declare const SDK_WARNINGS: {
103
103
  readonly causes: readonly ["Application called answer() twice on the same call object", "Multiple click handlers or event listeners triggering answer()"];
104
104
  readonly solutions: readonly ["Ensure answer() is called only once per call", "Disable the answer button after the first click", "Check that answer() is not invoked from multiple event handlers"];
105
105
  };
106
+ readonly 33008: {
107
+ readonly name: "ICE_CANDIDATE_PAIR_CHANGED";
108
+ readonly message: "ICE candidate pair changed mid-call";
109
+ readonly description: "The selected ICE candidate pair changed during an active call. This indicates a network path shift — for example, a Wi-Fi to cellular handoff, a NAT rebinding, or a relay fallback. The call may continue normally, but the path change can briefly affect audio quality.";
110
+ readonly causes: readonly ["Network interface change (e.g. Wi-Fi to cellular)", "NAT rebinding or IP address change", "Previous candidate pair failed and ICE selected an alternative", "Network topology change"];
111
+ readonly solutions: readonly ["Monitor for audio quality degradation after the path change", "Check network stability if changes are frequent", "Verify TURN server configuration for relay fallback"];
112
+ };
106
113
  readonly 33007: {
107
114
  readonly name: "DUPLICATE_INBOUND_ANSWER";
108
115
  readonly message: "Call answer ignored because another inbound call is already being answered";
@@ -117,6 +124,34 @@ export declare const SDK_WARNINGS: {
117
124
  readonly causes: readonly ["Token was issued with a limited lifetime"];
118
125
  readonly solutions: readonly ["Generate a new authentication token", "Reconnect with fresh credentials before the token expires"];
119
126
  };
127
+ readonly 36001: {
128
+ readonly name: "SIGNALING_HEALTH_PROBE_TIMEOUT";
129
+ readonly message: "Signaling health probe timed out";
130
+ readonly description: "A signaling liveness probe (Ping) was sent during an active call but no response was received within the timeout window. This indicates the WebSocket may be half-dead — the browser reports it as OPEN but data is not flowing. The SDK will force-close the socket to trigger reconnection.";
131
+ readonly causes: readonly ["Network interface removed mid-call while another interface remains", "TCP connection bound to a removed IP/route became half-dead", "Firewall or NAT state expired silently", "WebSocket proxy or load balancer dropped the connection without a close frame"];
132
+ readonly solutions: readonly ["The SDK will automatically force-close the socket and reconnect", "Check for network interface changes during the call", "Verify firewall/NAT timeout settings"];
133
+ };
134
+ readonly 36002: {
135
+ readonly name: "SIGNALING_REQUEST_TIMEOUT";
136
+ readonly message: "Signaling request timed out";
137
+ readonly description: "A signaling-critical JSON-RPC request (e.g. ICE restart Modify) did not receive a response within the timeout window while the socket reports OPEN. This may indicate the WebSocket is half-dead or the server is unresponsive. The SDK will mark signaling as unhealthy and force reconnection.";
138
+ readonly causes: readonly ["Half-dead WebSocket (browser reports OPEN but data does not flow)", "Server unresponsive or overloaded", "Network path interruption without TCP RST"];
139
+ readonly solutions: readonly ["The SDK will automatically force-close the socket and reconnect", "Check server health and response times", "Check for network path issues"];
140
+ };
141
+ readonly 36003: {
142
+ readonly name: "SIGNALING_RECOVERY_REQUIRED";
143
+ readonly message: "Signaling recovery required";
144
+ readonly description: "The signaling (WebSocket) path has been detected as unhealthy and the SDK will force-close the socket and reconnect. If media was still flowing, the reconnect is delayed briefly to allow the application to notify the user about a short interruption. Active calls will be recovered via reattach after reconnection.";
145
+ readonly causes: readonly ["WebSocket probe timed out with no response", "Critical signaling request timed out", "Peer/media failure detected while signaling is also unhealthy"];
146
+ readonly solutions: readonly ["The SDK will automatically reconnect and recover the call", "Check for network interface changes or interruptions", "Verify firewall/NAT timeout settings"];
147
+ };
148
+ readonly 36004: {
149
+ readonly name: "MEDIA_RECOVERY_REQUIRED";
150
+ readonly message: "Media recovery required";
151
+ readonly description: "The peer connection or media flow has been detected as unhealthy while signaling is healthy. The SDK will attempt ICE restart to recover the media path. No socket reconnection is needed.";
152
+ readonly causes: readonly ["ICE connection state changed to failed", "RTCPeerConnection state changed to failed", "No RTP packets/bytes received while media should be active"];
153
+ readonly solutions: readonly ["The SDK will automatically attempt ICE restart", "Check network connectivity and ICE candidate availability", "Verify TURN server configuration"];
154
+ };
120
155
  readonly 35001: {
121
156
  readonly name: "SESSION_NOT_REATTACHED";
122
157
  readonly message: "Active call lost after reconnect";
@@ -46,3 +46,15 @@ export declare class TelnyxError extends Error implements ITelnyxError {
46
46
  export declare function isMediaRecoveryErrorEvent(event: ITelnyxErrorEvent): event is ITelnyxMediaRecoveryErrorEvent;
47
47
  export declare function classifyMediaErrorCode(error: unknown): typeof MEDIA_MICROPHONE_PERMISSION_DENIED | typeof MEDIA_DEVICE_NOT_FOUND | typeof MEDIA_GET_USER_MEDIA_FAILED;
48
48
  export declare function createTelnyxError(code: SdkErrorCode, originalError?: unknown, message?: string): TelnyxError;
49
+ export declare class RequestTimeoutError extends Error {
50
+ readonly requestId: string;
51
+ readonly timeoutMs: number;
52
+ readonly method: string;
53
+ constructor(requestId: string, timeoutMs: number, method?: string);
54
+ }
55
+ export declare class StaleRequestError extends Error {
56
+ readonly requestId: string;
57
+ readonly staleGeneration: number;
58
+ readonly currentGeneration: number;
59
+ constructor(requestId: string, staleGeneration: number, currentGeneration: number);
60
+ }
@@ -0,0 +1,10 @@
1
+ import type Connection from '../../services/Connection';
2
+ export declare type PeerFailureEvidence = 'ice_failed' | 'connection_failed';
3
+ export interface ISignalingHealthSession {
4
+ uuid: string;
5
+ sessionid: string;
6
+ connection: Connection | null;
7
+ hasActiveCall(): boolean;
8
+ socketDisconnect(): void;
9
+ triggerIceRestart(callId: string): void;
10
+ }
@@ -57,6 +57,7 @@ export default abstract class BaseCall implements IWebRTCCall {
57
57
  get remoteStream(): MediaStream;
58
58
  get memberChannel(): string;
59
59
  get isAudioMuted(): boolean;
60
+ private _hasActiveUnmutedLocalAudioTrack;
60
61
  shouldForceRelayCandidateForRecovery(): boolean;
61
62
  invite(): Promise<void>;
62
63
  answer(params?: AnswerParams): Promise<void>;
@@ -145,7 +145,7 @@ export declare class CallReportCollector {
145
145
  private intervalRTTs;
146
146
  private intervalBitrates;
147
147
  private previousStats;
148
- private previousCandidatePairId;
148
+ private previousCandidatePairSnapshot;
149
149
  private static readonly INITIAL_COLLECTION_INTERVAL_MS;
150
150
  private static readonly INITIAL_COLLECTION_DURATION_MS;
151
151
  private readonly MAX_BUFFER_SIZE;
@@ -167,6 +167,7 @@ export declare class CallReportCollector {
167
167
  private static readonly WARNING_THROTTLE_MS;
168
168
  private _prevPacketsReceived;
169
169
  private _prevPacketsLost;
170
+ private _previousStatsEntryForWarnings;
170
171
  private _lastLocalAudioTrackSnapshotJson;
171
172
  private _hasConfirmedLocalAudio;
172
173
  private _confirmedLocalAudioSilenceMs;
@@ -29,6 +29,7 @@ export default class Peer {
29
29
  private _offlineHandler;
30
30
  constructor(type: PeerType, options: IVertoCallOptions, session: BrowserSession, trickleIceSdpFn: (sdp: RTCSessionDescriptionInit) => void, registerPeerEvents: (instance: RTCPeerConnection) => void);
31
31
  finishIceRestart(): void;
32
+ restartIce(): boolean;
32
33
  get isOffer(): boolean;
33
34
  get isAnswer(): boolean;
34
35
  get isDebugEnabled(): boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telnyx/webrtc",
3
- "version": "2.27.0-beta.5",
3
+ "version": "2.27.0-beta.6",
4
4
  "description": "Telnyx WebRTC Client",
5
5
  "keywords": [
6
6
  "telnyx",