@phystack/hub-client 4.5.19-dev → 4.5.20-dev
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/index.d.ts +22 -28
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +252 -378
- package/dist/index.js.map +1 -1
- package/dist/peripheral-twin.d.ts +34 -0
- package/dist/peripheral-twin.d.ts.map +1 -0
- package/dist/peripheral-twin.js +234 -0
- package/dist/peripheral-twin.js.map +1 -0
- package/dist/services/phyhub-connection.service.d.ts +1 -0
- package/dist/services/phyhub-connection.service.d.ts.map +1 -1
- package/dist/services/phyhub-connection.service.js +17 -0
- package/dist/services/phyhub-connection.service.js.map +1 -1
- package/dist/services/phyhub-direct-connection.service.d.ts +21 -0
- package/dist/services/phyhub-direct-connection.service.d.ts.map +1 -0
- package/dist/services/phyhub-direct-connection.service.js +101 -0
- package/dist/services/phyhub-direct-connection.service.js.map +1 -0
- package/dist/services/webrtc/data-channel-handler.d.ts +45 -0
- package/dist/services/webrtc/data-channel-handler.d.ts.map +1 -0
- package/dist/services/webrtc/data-channel-handler.js +260 -0
- package/dist/services/webrtc/data-channel-handler.js.map +1 -0
- package/dist/services/webrtc/index.d.ts +8 -0
- package/dist/services/webrtc/index.d.ts.map +1 -0
- package/dist/services/webrtc/index.js +18 -0
- package/dist/services/webrtc/index.js.map +1 -0
- package/dist/services/webrtc/media-stream-handler.d.ts +57 -0
- package/dist/services/webrtc/media-stream-handler.d.ts.map +1 -0
- package/dist/services/webrtc/media-stream-handler.js +383 -0
- package/dist/services/webrtc/media-stream-handler.js.map +1 -0
- package/dist/services/webrtc/peer-connection-manager.d.ts +40 -0
- package/dist/services/webrtc/peer-connection-manager.d.ts.map +1 -0
- package/dist/services/webrtc/peer-connection-manager.js +336 -0
- package/dist/services/webrtc/peer-connection-manager.js.map +1 -0
- package/dist/services/webrtc/types.d.ts +134 -0
- package/dist/services/webrtc/types.d.ts.map +1 -0
- package/dist/services/webrtc/types.js +12 -0
- package/dist/services/webrtc/types.js.map +1 -0
- package/dist/services/webrtc/webrtc-globals.d.ts +4 -0
- package/dist/services/webrtc/webrtc-globals.d.ts.map +1 -0
- package/dist/services/webrtc/webrtc-globals.js +72 -0
- package/dist/services/webrtc/webrtc-globals.js.map +1 -0
- package/dist/services/webrtc/webrtc-manager.d.ts +35 -0
- package/dist/services/webrtc/webrtc-manager.d.ts.map +1 -0
- package/dist/services/webrtc/webrtc-manager.js +274 -0
- package/dist/services/webrtc/webrtc-manager.js.map +1 -0
- package/dist/test/communication-comprehensive-test.d.ts +8 -0
- package/dist/test/communication-comprehensive-test.d.ts.map +1 -0
- package/dist/test/communication-comprehensive-test.js +356 -0
- package/dist/test/communication-comprehensive-test.js.map +1 -0
- package/dist/test/webrtc-channel-names-test.d.ts +2 -0
- package/dist/test/webrtc-channel-names-test.d.ts.map +1 -0
- package/dist/test/webrtc-channel-names-test.js +177 -0
- package/dist/test/webrtc-channel-names-test.js.map +1 -0
- package/dist/test/webrtc-comprehensive-test.d.ts +2 -0
- package/dist/test/webrtc-comprehensive-test.d.ts.map +1 -0
- package/dist/test/webrtc-comprehensive-test.js +328 -0
- package/dist/test/webrtc-comprehensive-test.js.map +1 -0
- package/dist/test/webrtc-reconnect-test.d.ts +4 -0
- package/dist/test/webrtc-reconnect-test.d.ts.map +1 -0
- package/dist/test/webrtc-reconnect-test.js +244 -0
- package/dist/test/webrtc-reconnect-test.js.map +1 -0
- package/dist/test/webrtc-test-harness.d.ts +4 -0
- package/dist/test/webrtc-test-harness.d.ts.map +1 -0
- package/dist/test/webrtc-test-harness.js +169 -0
- package/dist/test/webrtc-test-harness.js.map +1 -0
- package/dist/twin-messaging.d.ts +20 -0
- package/dist/twin-messaging.d.ts.map +1 -0
- package/dist/twin-messaging.js +94 -0
- package/dist/twin-messaging.js.map +1 -0
- package/dist/twin-registry.d.ts +9 -0
- package/dist/twin-registry.d.ts.map +1 -0
- package/dist/twin-registry.js +26 -0
- package/dist/twin-registry.js.map +1 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +20 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/twin.types.d.ts +62 -14
- package/dist/types/twin.types.d.ts.map +1 -1
- package/dist/types/twin.types.js +8 -1
- package/dist/types/twin.types.js.map +1 -1
- package/docs/webrtc-howto.md +398 -0
- package/docs/webrtc-test.md +330 -0
- package/package.json +3 -3
- package/scripts/webrtc-test.sh +401 -0
- package/src/index.ts +378 -568
- package/src/peripheral-twin.ts +337 -0
- package/src/services/phyhub-connection.service.ts +24 -0
- package/src/services/phyhub-direct-connection.service.ts +159 -0
- package/src/services/webrtc/data-channel-handler.ts +362 -0
- package/src/services/webrtc/index.ts +36 -0
- package/src/services/webrtc/media-stream-handler.ts +536 -0
- package/src/services/webrtc/peer-connection-manager.ts +467 -0
- package/src/services/webrtc/types.ts +273 -0
- package/src/services/webrtc/webrtc-globals.ts +108 -0
- package/src/services/webrtc/webrtc-manager.ts +490 -0
- package/src/test/communication-comprehensive-test.ts +533 -0
- package/src/test/webrtc-channel-names-test.ts +266 -0
- package/src/test/webrtc-comprehensive-test.ts +494 -0
- package/src/test/webrtc-reconnect-test.ts +345 -0
- package/src/test/webrtc-test-harness.ts +254 -0
- package/src/twin-messaging.ts +184 -0
- package/src/twin-registry.ts +39 -0
- package/src/types/index.ts +3 -0
- package/src/types/twin.types.ts +80 -14
- package/dist/services/webrtc/datachannel.d.ts +0 -10
- package/dist/services/webrtc/datachannel.d.ts.map +0 -1
- package/dist/services/webrtc/datachannel.js +0 -290
- package/dist/services/webrtc/datachannel.js.map +0 -1
- package/dist/services/webrtc/mediastream.d.ts +0 -10
- package/dist/services/webrtc/mediastream.d.ts.map +0 -1
- package/dist/services/webrtc/mediastream.js +0 -396
- package/dist/services/webrtc/mediastream.js.map +0 -1
- package/dist/services/webrtc/peer-connection-ice.d.ts +0 -32
- package/dist/services/webrtc/peer-connection-ice.d.ts.map +0 -1
- package/dist/services/webrtc/peer-connection-ice.js +0 -483
- package/dist/services/webrtc/peer-connection-ice.js.map +0 -1
- package/src/services/webrtc/datachannel.ts +0 -421
- package/src/services/webrtc/mediastream.ts +0 -602
- package/src/services/webrtc/peer-connection-ice.ts +0 -689
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PeerConnectionManager
|
|
3
|
+
*
|
|
4
|
+
* Manages RTCPeerConnection lifecycle including:
|
|
5
|
+
* - Connection creation and teardown
|
|
6
|
+
* - ICE candidate gathering and exchange
|
|
7
|
+
* - Connection state monitoring
|
|
8
|
+
* - Automatic reconnection with exponential backoff
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { ensureWebRTCGlobals } from './webrtc-globals';
|
|
12
|
+
import {
|
|
13
|
+
TwinMessagingInterface,
|
|
14
|
+
PeerConnectionConfig,
|
|
15
|
+
SignalingMessageType,
|
|
16
|
+
DEFAULT_WEBRTC_OPTIONS,
|
|
17
|
+
} from './types';
|
|
18
|
+
|
|
19
|
+
export interface PeerConnectionManagerOptions {
|
|
20
|
+
connectionTimeout: number;
|
|
21
|
+
initialRetryDelay: number;
|
|
22
|
+
maxRetryDelay: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class PeerConnectionManager {
|
|
26
|
+
private pc: RTCPeerConnection | null = null;
|
|
27
|
+
private config: PeerConnectionConfig;
|
|
28
|
+
private twinMessaging: TwinMessagingInterface;
|
|
29
|
+
private options: PeerConnectionManagerOptions;
|
|
30
|
+
|
|
31
|
+
private isShuttingDown = false;
|
|
32
|
+
private isReconnecting = false;
|
|
33
|
+
private connectionId = 0;
|
|
34
|
+
private pendingCandidates: RTCIceCandidateInit[] = [];
|
|
35
|
+
private connectionTimeout: NodeJS.Timeout | null = null;
|
|
36
|
+
private messageHandler: ((message: any) => void) | null = null;
|
|
37
|
+
|
|
38
|
+
constructor(
|
|
39
|
+
config: PeerConnectionConfig,
|
|
40
|
+
twinMessaging: TwinMessagingInterface,
|
|
41
|
+
options: Partial<PeerConnectionManagerOptions> = {}
|
|
42
|
+
) {
|
|
43
|
+
this.config = config;
|
|
44
|
+
this.twinMessaging = twinMessaging;
|
|
45
|
+
this.options = {
|
|
46
|
+
connectionTimeout: options.connectionTimeout ?? DEFAULT_WEBRTC_OPTIONS.connectionTimeout,
|
|
47
|
+
initialRetryDelay: options.initialRetryDelay ?? DEFAULT_WEBRTC_OPTIONS.initialRetryDelay,
|
|
48
|
+
maxRetryDelay: options.maxRetryDelay ?? DEFAULT_WEBRTC_OPTIONS.maxRetryDelay,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Start the WebRTC connection process.
|
|
54
|
+
* Returns the RTCPeerConnection once connected.
|
|
55
|
+
*/
|
|
56
|
+
async connect(): Promise<RTCPeerConnection> {
|
|
57
|
+
await ensureWebRTCGlobals();
|
|
58
|
+
|
|
59
|
+
if (this.pc && this.pc.connectionState === 'connected') {
|
|
60
|
+
return this.pc;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return this.attemptConnection(0, this.config.useStun, this.options.initialRetryDelay);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get the current RTCPeerConnection (may be null if not connected)
|
|
68
|
+
*/
|
|
69
|
+
getPeerConnection(): RTCPeerConnection | null {
|
|
70
|
+
return this.pc;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Check if the connection is currently active
|
|
75
|
+
*/
|
|
76
|
+
isConnected(): boolean {
|
|
77
|
+
return this.pc?.connectionState === 'connected';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Close the connection and clean up resources
|
|
82
|
+
*/
|
|
83
|
+
close(): void {
|
|
84
|
+
this.isShuttingDown = true;
|
|
85
|
+
this.cleanup();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Trigger a manual reconnection
|
|
90
|
+
*/
|
|
91
|
+
reconnect(): void {
|
|
92
|
+
if (this.isReconnecting || this.isShuttingDown) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
this.triggerReconnect(0);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ===========================================================================
|
|
99
|
+
// Private Methods
|
|
100
|
+
// ===========================================================================
|
|
101
|
+
|
|
102
|
+
private async attemptConnection(
|
|
103
|
+
retryCount: number,
|
|
104
|
+
useStun: boolean,
|
|
105
|
+
delay: number
|
|
106
|
+
): Promise<RTCPeerConnection> {
|
|
107
|
+
if (this.isShuttingDown) {
|
|
108
|
+
throw new Error('Connection attempt cancelled - shutting down');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return new Promise((resolve, reject) => {
|
|
112
|
+
const currentConnectionId = ++this.connectionId;
|
|
113
|
+
let connectionEstablished = false;
|
|
114
|
+
|
|
115
|
+
// Create peer connection FIRST, before setting up message handler
|
|
116
|
+
console.log(`[PeerConnectionManager] Creating peer connection (useStun=${useStun}, isInitiator=${this.config.isInitiator})`);
|
|
117
|
+
this.pc = this.createPeerConnection(useStun);
|
|
118
|
+
const pc = this.pc;
|
|
119
|
+
console.log(`[PeerConnectionManager] Peer connection created: ${pc.connectionState}`);
|
|
120
|
+
|
|
121
|
+
// Setup message handler for signaling
|
|
122
|
+
this.setupMessageHandler(currentConnectionId);
|
|
123
|
+
|
|
124
|
+
// Subscribe to the target twin
|
|
125
|
+
this.twinMessaging.subscribe(this.config.targetTwinId).catch(err => {
|
|
126
|
+
console.error('[PeerConnectionManager] Failed to subscribe to twin:', err);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Setup ICE handling
|
|
130
|
+
this.setupIceHandling(pc, currentConnectionId);
|
|
131
|
+
|
|
132
|
+
// Setup connection state monitoring
|
|
133
|
+
pc.onconnectionstatechange = () => {
|
|
134
|
+
this.config.onSignalingStateChange?.(pc.signalingState);
|
|
135
|
+
|
|
136
|
+
if (pc.connectionState === 'connected') {
|
|
137
|
+
connectionEstablished = true;
|
|
138
|
+
this.clearConnectionTimeout();
|
|
139
|
+
this.config.onConnected();
|
|
140
|
+
resolve(pc);
|
|
141
|
+
} else if (
|
|
142
|
+
pc.connectionState === 'failed' ||
|
|
143
|
+
pc.connectionState === 'disconnected' ||
|
|
144
|
+
pc.connectionState === 'closed'
|
|
145
|
+
) {
|
|
146
|
+
if (connectionEstablished && !this.isShuttingDown) {
|
|
147
|
+
this.config.onDisconnected();
|
|
148
|
+
this.triggerReconnect(0);
|
|
149
|
+
} else if (!connectionEstablished) {
|
|
150
|
+
this.handleConnectionFailure(retryCount, useStun, delay, resolve, reject);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
pc.oniceconnectionstatechange = () => {
|
|
156
|
+
this.config.onIceStateChange?.(pc.iceConnectionState);
|
|
157
|
+
|
|
158
|
+
if (pc.iceConnectionState === 'failed' && connectionEstablished) {
|
|
159
|
+
this.triggerReconnect(0);
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
// Call the setup callback (for adding data channels/tracks) before creating offer
|
|
164
|
+
if (this.config.onPeerConnectionCreated) {
|
|
165
|
+
try {
|
|
166
|
+
const result = this.config.onPeerConnectionCreated(pc);
|
|
167
|
+
if (result instanceof Promise) {
|
|
168
|
+
result.catch(err => console.error('[PeerConnectionManager] Error in onPeerConnectionCreated:', err));
|
|
169
|
+
}
|
|
170
|
+
} catch (err) {
|
|
171
|
+
console.error('[PeerConnectionManager] Error in onPeerConnectionCreated:', err);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Start the offer/answer exchange if we're the initiator
|
|
176
|
+
if (this.config.isInitiator) {
|
|
177
|
+
this.createAndSendOffer(pc).catch(err => {
|
|
178
|
+
console.error('[PeerConnectionManager] Error creating offer:', err);
|
|
179
|
+
this.handleConnectionFailure(retryCount, useStun, delay, resolve, reject);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Set connection timeout
|
|
184
|
+
this.connectionTimeout = setTimeout(() => {
|
|
185
|
+
if (!connectionEstablished) {
|
|
186
|
+
console.log(`[PeerConnectionManager] Connection timeout (${useStun ? 'with' : 'without'} STUN)`);
|
|
187
|
+
this.handleConnectionFailure(retryCount, useStun, delay, resolve, reject);
|
|
188
|
+
}
|
|
189
|
+
}, this.options.connectionTimeout);
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private createPeerConnection(useStun: boolean): RTCPeerConnection {
|
|
194
|
+
const iceServers = useStun
|
|
195
|
+
? this.config.stunServers.map(url => ({ urls: url }))
|
|
196
|
+
: [];
|
|
197
|
+
|
|
198
|
+
return new RTCPeerConnection({ iceServers });
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private setupIceHandling(pc: RTCPeerConnection, connectionId: number): void {
|
|
202
|
+
pc.onicecandidate = async event => {
|
|
203
|
+
if (this.connectionId !== connectionId) return;
|
|
204
|
+
|
|
205
|
+
console.log('[PeerConnectionManager] ICE candidate:', event.candidate?.candidate || 'gathering complete');
|
|
206
|
+
|
|
207
|
+
this.config.onIceCandidate?.(event.candidate);
|
|
208
|
+
|
|
209
|
+
if (event.candidate) {
|
|
210
|
+
try {
|
|
211
|
+
console.log('[PeerConnectionManager] Sending ICE candidate');
|
|
212
|
+
await this.sendSignalingMessage('ice', event.candidate);
|
|
213
|
+
} catch (err) {
|
|
214
|
+
console.error('[PeerConnectionManager] Error sending ICE candidate:', err);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
pc.onicegatheringstatechange = () => {
|
|
220
|
+
console.log('[PeerConnectionManager] ICE gathering state:', pc.iceGatheringState);
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
pc.oniceconnectionstatechange = () => {
|
|
224
|
+
console.log('[PeerConnectionManager] ICE connection state:', pc.iceConnectionState);
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private setupMessageHandler(connectionId: number): void {
|
|
229
|
+
const ownTwinId = this.twinMessaging.getOwnTwinId();
|
|
230
|
+
|
|
231
|
+
// Remove old handler if exists
|
|
232
|
+
if (this.messageHandler) {
|
|
233
|
+
this.twinMessaging.offMessage(ownTwinId, this.messageHandler);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
this.messageHandler = async (message: any) => {
|
|
237
|
+
if (this.isShuttingDown || this.connectionId !== connectionId) return;
|
|
238
|
+
|
|
239
|
+
// Filter: only process messages FROM the target peer
|
|
240
|
+
if (message.sourceTwinId !== this.config.targetTwinId) return;
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
await this.handleSignalingMessage(message);
|
|
244
|
+
} catch (err) {
|
|
245
|
+
console.error('[PeerConnectionManager] Error handling signaling message:', err);
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
// Register on OWN twinId, filter by sourceTwinId in callback
|
|
250
|
+
this.twinMessaging.onMessage(ownTwinId, this.messageHandler);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private async handleSignalingMessage(message: any): Promise<void> {
|
|
254
|
+
if (!this.pc) return;
|
|
255
|
+
|
|
256
|
+
const messageType = message.data?.type as string;
|
|
257
|
+
if (!messageType) return;
|
|
258
|
+
|
|
259
|
+
// Extract the signal type (offer/answer/ice) from the message type
|
|
260
|
+
// Message type format: channel-{twinId}:{signalType}
|
|
261
|
+
// We match on the signal type suffix since the channelId varies by perspective
|
|
262
|
+
const prefix = `${this.config.channelPrefix}-`;
|
|
263
|
+
if (!messageType.startsWith(prefix)) return;
|
|
264
|
+
|
|
265
|
+
const signalType = messageType.split(':').pop();
|
|
266
|
+
console.log(`[PeerConnectionManager] Received signaling message: ${signalType}`);
|
|
267
|
+
|
|
268
|
+
switch (signalType) {
|
|
269
|
+
case 'offer':
|
|
270
|
+
await this.handleOffer(message.data.data);
|
|
271
|
+
break;
|
|
272
|
+
|
|
273
|
+
case 'answer':
|
|
274
|
+
await this.handleAnswer(message.data.data);
|
|
275
|
+
break;
|
|
276
|
+
|
|
277
|
+
case 'ice':
|
|
278
|
+
await this.handleIceCandidate(message.data.data);
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private async handleOffer(offer: RTCSessionDescriptionInit): Promise<void> {
|
|
284
|
+
if (!this.pc || this.config.isInitiator) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const pc = this.pc;
|
|
289
|
+
|
|
290
|
+
if (pc.signalingState === 'stable' || pc.signalingState === 'have-remote-offer') {
|
|
291
|
+
if (pc.signalingState === 'have-remote-offer') {
|
|
292
|
+
await pc.setLocalDescription({ type: 'rollback' });
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
console.log('[PeerConnectionManager] Setting remote description (offer)');
|
|
296
|
+
await pc.setRemoteDescription(offer);
|
|
297
|
+
console.log(`[PeerConnectionManager] Remote description set. signalingState=${pc.signalingState}`);
|
|
298
|
+
await this.applyPendingCandidates();
|
|
299
|
+
|
|
300
|
+
console.log('[PeerConnectionManager] Creating answer...');
|
|
301
|
+
const answer = await pc.createAnswer();
|
|
302
|
+
console.log(`[PeerConnectionManager] Answer created. type=${answer.type}`);
|
|
303
|
+
|
|
304
|
+
console.log('[PeerConnectionManager] Setting local description (answer)');
|
|
305
|
+
await pc.setLocalDescription(answer);
|
|
306
|
+
console.log(`[PeerConnectionManager] Local description set. signalingState=${pc.signalingState}, iceGatheringState=${pc.iceGatheringState}`);
|
|
307
|
+
|
|
308
|
+
await this.sendSignalingMessage('answer', answer);
|
|
309
|
+
console.log('[PeerConnectionManager] Answer sent');
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
private async handleAnswer(answer: RTCSessionDescriptionInit): Promise<void> {
|
|
314
|
+
if (!this.pc || !this.config.isInitiator) return;
|
|
315
|
+
|
|
316
|
+
const pc = this.pc;
|
|
317
|
+
|
|
318
|
+
if (pc.signalingState === 'have-local-offer') {
|
|
319
|
+
await pc.setRemoteDescription(answer);
|
|
320
|
+
await this.applyPendingCandidates();
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
private async handleIceCandidate(candidate: RTCIceCandidateInit): Promise<void> {
|
|
325
|
+
if (!this.pc) return;
|
|
326
|
+
|
|
327
|
+
if (this.pc.remoteDescription) {
|
|
328
|
+
try {
|
|
329
|
+
await this.pc.addIceCandidate(candidate);
|
|
330
|
+
} catch (err) {
|
|
331
|
+
console.error('[PeerConnectionManager] Error adding ICE candidate:', err);
|
|
332
|
+
}
|
|
333
|
+
} else {
|
|
334
|
+
this.pendingCandidates.push(candidate);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
private async applyPendingCandidates(): Promise<void> {
|
|
339
|
+
if (!this.pc || this.pendingCandidates.length === 0) return;
|
|
340
|
+
|
|
341
|
+
for (const candidate of this.pendingCandidates) {
|
|
342
|
+
try {
|
|
343
|
+
await this.pc.addIceCandidate(candidate);
|
|
344
|
+
} catch (err) {
|
|
345
|
+
console.error('[PeerConnectionManager] Error applying pending ICE candidate:', err);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
this.pendingCandidates = [];
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
private async createAndSendOffer(pc: RTCPeerConnection): Promise<void> {
|
|
353
|
+
console.log('[PeerConnectionManager] Creating offer...');
|
|
354
|
+
const offer = await pc.createOffer();
|
|
355
|
+
console.log(`[PeerConnectionManager] Offer created. type=${offer.type}`);
|
|
356
|
+
|
|
357
|
+
console.log('[PeerConnectionManager] Setting local description (offer)');
|
|
358
|
+
await pc.setLocalDescription(offer);
|
|
359
|
+
console.log(`[PeerConnectionManager] Local description set. signalingState=${pc.signalingState}, iceGatheringState=${pc.iceGatheringState}`);
|
|
360
|
+
|
|
361
|
+
await this.sendSignalingMessage('offer', offer);
|
|
362
|
+
console.log('[PeerConnectionManager] Offer sent');
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
private async sendSignalingMessage(
|
|
366
|
+
type: SignalingMessageType,
|
|
367
|
+
data: RTCSessionDescriptionInit | RTCIceCandidateInit | RTCIceCandidate
|
|
368
|
+
): Promise<void> {
|
|
369
|
+
const channelId = `${this.config.channelPrefix}-${this.config.targetTwinId}`;
|
|
370
|
+
|
|
371
|
+
await this.twinMessaging.sendMessage(this.config.targetTwinId, {
|
|
372
|
+
type: `${channelId}:${type}`,
|
|
373
|
+
data,
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
private handleConnectionFailure(
|
|
378
|
+
retryCount: number,
|
|
379
|
+
currentUseStun: boolean,
|
|
380
|
+
delay: number,
|
|
381
|
+
resolve: (pc: RTCPeerConnection) => void,
|
|
382
|
+
reject: (error: Error) => void
|
|
383
|
+
): void {
|
|
384
|
+
this.cleanup();
|
|
385
|
+
|
|
386
|
+
if (this.isShuttingDown) {
|
|
387
|
+
reject(new Error('Connection cancelled - shutting down'));
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// First retry: toggle STUN setting
|
|
392
|
+
if (retryCount === 0) {
|
|
393
|
+
this.attemptConnection(1, !currentUseStun, this.options.initialRetryDelay)
|
|
394
|
+
.then(resolve)
|
|
395
|
+
.catch(reject);
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Subsequent retries: exponential backoff
|
|
400
|
+
const nextDelay = Math.min(this.options.maxRetryDelay, delay * 2);
|
|
401
|
+
|
|
402
|
+
setTimeout(() => {
|
|
403
|
+
this.attemptConnection(retryCount + 1, currentUseStun, nextDelay)
|
|
404
|
+
.then(resolve)
|
|
405
|
+
.catch(reject);
|
|
406
|
+
}, delay);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
private triggerReconnect(attempt: number): void {
|
|
410
|
+
if (this.isReconnecting || this.isShuttingDown) return;
|
|
411
|
+
|
|
412
|
+
this.isReconnecting = true;
|
|
413
|
+
this.config.onReconnecting?.(attempt);
|
|
414
|
+
|
|
415
|
+
this.cleanup();
|
|
416
|
+
|
|
417
|
+
const reconnectDelay = Math.min(
|
|
418
|
+
this.options.maxRetryDelay,
|
|
419
|
+
this.options.initialRetryDelay * Math.pow(2, attempt)
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
setTimeout(() => {
|
|
423
|
+
this.attemptConnection(0, !this.config.useStun, this.options.initialRetryDelay)
|
|
424
|
+
.then(() => {
|
|
425
|
+
this.isReconnecting = false;
|
|
426
|
+
this.config.onReconnected?.(attempt);
|
|
427
|
+
})
|
|
428
|
+
.catch(err => {
|
|
429
|
+
console.error(`[PeerConnectionManager] Reconnection attempt ${attempt} failed:`, err);
|
|
430
|
+
this.triggerReconnect(attempt + 1);
|
|
431
|
+
});
|
|
432
|
+
}, reconnectDelay);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
private clearConnectionTimeout(): void {
|
|
436
|
+
if (this.connectionTimeout) {
|
|
437
|
+
clearTimeout(this.connectionTimeout);
|
|
438
|
+
this.connectionTimeout = null;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
private cleanup(): void {
|
|
443
|
+
this.clearConnectionTimeout();
|
|
444
|
+
|
|
445
|
+
if (this.messageHandler) {
|
|
446
|
+
const ownTwinId = this.twinMessaging.getOwnTwinId();
|
|
447
|
+
this.twinMessaging.offMessage(ownTwinId, this.messageHandler);
|
|
448
|
+
this.messageHandler = null;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (this.pc) {
|
|
452
|
+
this.pc.onicecandidate = null;
|
|
453
|
+
this.pc.onconnectionstatechange = null;
|
|
454
|
+
this.pc.oniceconnectionstatechange = null;
|
|
455
|
+
|
|
456
|
+
try {
|
|
457
|
+
this.pc.close();
|
|
458
|
+
} catch (err) {
|
|
459
|
+
console.error('[PeerConnectionManager] Error closing peer connection:', err);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
this.pc = null;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
this.pendingCandidates = [];
|
|
466
|
+
}
|
|
467
|
+
}
|