@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
|
@@ -1,689 +0,0 @@
|
|
|
1
|
-
// Initialize WebRTC globals if we're in Node environment
|
|
2
|
-
(async function initializeWebRTCGlobals() {
|
|
3
|
-
// Only run in Node environment
|
|
4
|
-
if (typeof window !== 'undefined') return;
|
|
5
|
-
|
|
6
|
-
try {
|
|
7
|
-
// Dynamically import wrtc
|
|
8
|
-
const wrtc = require('@roamhq/wrtc');
|
|
9
|
-
|
|
10
|
-
// Set all WebRTC globals
|
|
11
|
-
if (typeof global.MediaStream === 'undefined' && wrtc.MediaStream) {
|
|
12
|
-
global.MediaStream = wrtc.MediaStream;
|
|
13
|
-
console.log('Global MediaStream initialized');
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
if (typeof global.RTCDataChannel === 'undefined' && wrtc.RTCDataChannel) {
|
|
17
|
-
global.RTCDataChannel = wrtc.RTCDataChannel;
|
|
18
|
-
console.log('Global RTCDataChannel initialized');
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
if (typeof global.RTCPeerConnection === 'undefined' && wrtc.RTCPeerConnection) {
|
|
22
|
-
global.RTCPeerConnection = wrtc.RTCPeerConnection;
|
|
23
|
-
console.log('Global RTCPeerConnection initialized');
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
if (typeof global.RTCSessionDescription === 'undefined' && wrtc.RTCSessionDescription) {
|
|
27
|
-
global.RTCSessionDescription = wrtc.RTCSessionDescription;
|
|
28
|
-
console.log('Global RTCSessionDescription initialized');
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
if (typeof global.RTCIceCandidate === 'undefined' && wrtc.RTCIceCandidate) {
|
|
32
|
-
global.RTCIceCandidate = wrtc.RTCIceCandidate;
|
|
33
|
-
console.log('Global RTCIceCandidate initialized');
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
console.log('WebRTC globals successfully initialized');
|
|
37
|
-
} catch (error) {
|
|
38
|
-
console.error('Failed to initialize WebRTC globals:', error);
|
|
39
|
-
}
|
|
40
|
-
})().catch(err => console.error('Error in WebRTC globals initialization:', err));
|
|
41
|
-
|
|
42
|
-
import { WebRTCConnectionOptions } from '../../types/webrtc.types';
|
|
43
|
-
|
|
44
|
-
export interface PeerConnectionState {
|
|
45
|
-
pc: RTCPeerConnection | null;
|
|
46
|
-
isShuttingDown: boolean;
|
|
47
|
-
isReconnecting: boolean;
|
|
48
|
-
currentConnectionId: number;
|
|
49
|
-
pendingCandidates: RTCIceCandidateInit[];
|
|
50
|
-
activeDataChannel: RTCDataChannel | null;
|
|
51
|
-
activeMediaStream?: MediaStream | null;
|
|
52
|
-
useStun?: boolean;
|
|
53
|
-
channelPrefix?: string;
|
|
54
|
-
targetTwinId?: string;
|
|
55
|
-
isInitiator?: boolean;
|
|
56
|
-
connectionType?: 'datachannel' | 'mediastream';
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export interface PeerConnectionCallbacks {
|
|
60
|
-
onCreateConnection: (
|
|
61
|
-
pc: RTCPeerConnection,
|
|
62
|
-
connectionId: number,
|
|
63
|
-
isInitiator: boolean
|
|
64
|
-
) => Promise<RTCDataChannel | MediaStream | null>;
|
|
65
|
-
|
|
66
|
-
onCloseCleanup: () => void;
|
|
67
|
-
onSubscribe: () => Promise<void>;
|
|
68
|
-
sendSignalingMessage: (type: string, data: any) => Promise<void>;
|
|
69
|
-
setupMessageHandling: (messageHandler: (message: any) => void) => void;
|
|
70
|
-
removeMessageHandling: (messageHandler: (message: any) => void) => void;
|
|
71
|
-
reportConnectionState: (info: string) => void;
|
|
72
|
-
reportError: (info: string, error?: any) => void;
|
|
73
|
-
retryConnection: (retryCount: number, useStun?: boolean, delay?: number) => Promise<unknown>;
|
|
74
|
-
safeReject: (error: Error) => void;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Interface for RTCPeerConnection with optional experimental properties
|
|
79
|
-
*/
|
|
80
|
-
interface ExtendedRTCPeerConnection extends RTCPeerConnection {
|
|
81
|
-
// This is an experimental property that might not be available in all browsers/environments
|
|
82
|
-
onicecandidatepairchange?: (event: Event) => void;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Create a new RTCPeerConnection with or without STUN servers based on flags.
|
|
87
|
-
*/
|
|
88
|
-
export function createPeerConnection(useStun: boolean): RTCPeerConnection {
|
|
89
|
-
const pc = new RTCPeerConnection({
|
|
90
|
-
iceServers: useStun
|
|
91
|
-
? [{ urls: 'stun:stun.l.google.com:19302' }, { urls: 'stun:stun1.l.google.com:19302' }]
|
|
92
|
-
: [],
|
|
93
|
-
});
|
|
94
|
-
return pc;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Set up ICE candidate handlers and connection state handlers
|
|
99
|
-
* on the newly created RTCPeerConnection.
|
|
100
|
-
*/
|
|
101
|
-
export function handleIceAndConnectionState(
|
|
102
|
-
state: PeerConnectionState,
|
|
103
|
-
pc: RTCPeerConnection,
|
|
104
|
-
callbacks: PeerConnectionCallbacks,
|
|
105
|
-
connectionId: number
|
|
106
|
-
) {
|
|
107
|
-
const extendedPc = pc as ExtendedRTCPeerConnection;
|
|
108
|
-
|
|
109
|
-
pc.onicecandidate = async event => {
|
|
110
|
-
if (!pc) return;
|
|
111
|
-
// Only send ICE if this is still the active connection
|
|
112
|
-
if (event.candidate && state.currentConnectionId === connectionId) {
|
|
113
|
-
try {
|
|
114
|
-
// Log local ICE candidate info
|
|
115
|
-
logIceCandidate('Local ICE candidate', event.candidate);
|
|
116
|
-
await callbacks.sendSignalingMessage('ice', event.candidate);
|
|
117
|
-
} catch (err) {
|
|
118
|
-
// Swallow errors from ICE sending attempts
|
|
119
|
-
callbacks.reportError('Error sending ICE candidate:', err);
|
|
120
|
-
}
|
|
121
|
-
} else if (event.candidate === null) {
|
|
122
|
-
callbacks.reportConnectionState('ICE gathering complete');
|
|
123
|
-
}
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
// Add event handler for ICE candidate pair selection - this is an experimental feature
|
|
127
|
-
// and might not be available in all browsers/environments
|
|
128
|
-
extendedPc.onicecandidatepairchange = (_event: Event) => {
|
|
129
|
-
callbacks.reportConnectionState('ICE candidate pair changed');
|
|
130
|
-
|
|
131
|
-
// Get stats to determine the selected candidate pair
|
|
132
|
-
pc.getStats()
|
|
133
|
-
.then(stats => {
|
|
134
|
-
// Use any type for the stats objects to avoid TypeScript errors
|
|
135
|
-
// The RTCStatsReport interface varies across browsers and environments
|
|
136
|
-
const statsValues: any[] = [];
|
|
137
|
-
(stats as any).forEach((stat: any) => {
|
|
138
|
-
statsValues.push(stat);
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
// Find candidate pair and transport stats
|
|
142
|
-
const transportStats = statsValues.find(s => s.type === 'transport');
|
|
143
|
-
if (transportStats) {
|
|
144
|
-
callbacks.reportConnectionState(
|
|
145
|
-
`Transport selected: ${transportStats.selectedCandidatePairId}`
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const selectedPair = statsValues.find(s => s.type === 'candidate-pair' && s.selected);
|
|
150
|
-
if (selectedPair) {
|
|
151
|
-
callbacks.reportConnectionState(`Selected ICE candidate pair [${selectedPair.id}]:
|
|
152
|
-
local=${selectedPair.localCandidateId},
|
|
153
|
-
remote=${selectedPair.remoteCandidateId},
|
|
154
|
-
state=${selectedPair.state},
|
|
155
|
-
priority=${selectedPair.priority}`);
|
|
156
|
-
|
|
157
|
-
// Find the local and remote candidate details
|
|
158
|
-
const localCandidate = statsValues.find(
|
|
159
|
-
s => s.type === 'local-candidate' && s.id === selectedPair.localCandidateId
|
|
160
|
-
);
|
|
161
|
-
|
|
162
|
-
const remoteCandidate = statsValues.find(
|
|
163
|
-
s => s.type === 'remote-candidate' && s.id === selectedPair.remoteCandidateId
|
|
164
|
-
);
|
|
165
|
-
|
|
166
|
-
if (localCandidate) {
|
|
167
|
-
logIceCandidateStats('Selected local ICE candidate', localCandidate);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
if (remoteCandidate) {
|
|
171
|
-
logIceCandidateStats('Selected remote ICE candidate', remoteCandidate);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
})
|
|
175
|
-
.catch(err => {
|
|
176
|
-
callbacks.reportError('Error getting ICE candidate stats:', err);
|
|
177
|
-
});
|
|
178
|
-
};
|
|
179
|
-
|
|
180
|
-
pc.onconnectionstatechange = () => {
|
|
181
|
-
if (!pc) return;
|
|
182
|
-
callbacks.reportConnectionState(`Connection state: ${pc.connectionState}`);
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
pc.oniceconnectionstatechange = () => {
|
|
186
|
-
if (!pc) return;
|
|
187
|
-
callbacks.reportConnectionState(`ICE connection state: ${pc.iceConnectionState}`);
|
|
188
|
-
|
|
189
|
-
// When ICE connection state becomes 'connected', log the selected candidate pair
|
|
190
|
-
if (pc.iceConnectionState === 'connected') {
|
|
191
|
-
pc.getStats()
|
|
192
|
-
.then(stats => {
|
|
193
|
-
// Convert stats to array for easier processing
|
|
194
|
-
const statsValues: any[] = [];
|
|
195
|
-
(stats as any).forEach((stat: any) => {
|
|
196
|
-
statsValues.push(stat);
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
const selectedPair = statsValues.find(s => s.type === 'candidate-pair' && s.selected);
|
|
200
|
-
if (!selectedPair) return;
|
|
201
|
-
|
|
202
|
-
const localCandidate = statsValues.find(
|
|
203
|
-
s => s.type === 'local-candidate' && s.id === selectedPair.localCandidateId
|
|
204
|
-
);
|
|
205
|
-
|
|
206
|
-
const remoteCandidate = statsValues.find(
|
|
207
|
-
s => s.type === 'remote-candidate' && s.id === selectedPair.remoteCandidateId
|
|
208
|
-
);
|
|
209
|
-
|
|
210
|
-
if (selectedPair && localCandidate && remoteCandidate) {
|
|
211
|
-
callbacks.reportConnectionState('*** ICE CONNECTION ESTABLISHED ***');
|
|
212
|
-
callbacks.reportConnectionState(`Selected candidates:
|
|
213
|
-
Local: ${localCandidate.ip}:${localCandidate.port} (${localCandidate.protocol}) [${localCandidate.candidateType}]
|
|
214
|
-
Remote: ${remoteCandidate.ip}:${remoteCandidate.port} (${remoteCandidate.protocol}) [${remoteCandidate.candidateType}]
|
|
215
|
-
Round-trip time: ${selectedPair.currentRoundTripTime ? (selectedPair.currentRoundTripTime * 1000).toFixed(2) + 'ms' : 'unknown'}`);
|
|
216
|
-
}
|
|
217
|
-
})
|
|
218
|
-
.catch(err => {
|
|
219
|
-
callbacks.reportError('Error getting ICE candidate stats:', err);
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* Helper function to log ICE candidate details from RTCIceCandidate object
|
|
227
|
-
*/
|
|
228
|
-
function logIceCandidate(label: string, candidate: RTCIceCandidate) {
|
|
229
|
-
console.log(`${label}: ${candidate.candidate}`);
|
|
230
|
-
|
|
231
|
-
// Extract additional details for more verbose logging
|
|
232
|
-
// Example format: candidate:1467250027 1 udp 2122260223 192.168.0.1 56789 typ host generation 0
|
|
233
|
-
try {
|
|
234
|
-
const parts = candidate.candidate.split(' ');
|
|
235
|
-
if (parts.length > 7) {
|
|
236
|
-
const protocol = parts[2];
|
|
237
|
-
const ip = parts[4];
|
|
238
|
-
const port = parts[5];
|
|
239
|
-
const type = parts[7];
|
|
240
|
-
console.log(`Details: IP=${ip}, Port=${port}, Protocol=${protocol}, Type=${type}`);
|
|
241
|
-
}
|
|
242
|
-
} catch (e) {
|
|
243
|
-
// Ignore parsing errors
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* Helper function to log ICE candidate details from stats object
|
|
249
|
-
*/
|
|
250
|
-
function logIceCandidateStats(label: string, candidate: any) {
|
|
251
|
-
if (!candidate) return;
|
|
252
|
-
|
|
253
|
-
// Log basic information
|
|
254
|
-
console.log(
|
|
255
|
-
`${label}: ${candidate.ip}:${candidate.port} (${candidate.protocol}) [${candidate.candidateType}]`
|
|
256
|
-
);
|
|
257
|
-
|
|
258
|
-
// Log additional details if available
|
|
259
|
-
if (candidate.url) console.log(`STUN/TURN server: ${candidate.url}`);
|
|
260
|
-
if (candidate.priority) console.log(`Priority: ${candidate.priority}`);
|
|
261
|
-
if (candidate.networkType) console.log(`Network type: ${candidate.networkType}`);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
/**
|
|
265
|
-
* Attempts to start a WebRTC connection, sets up the PeerConnection,
|
|
266
|
-
* and calls data-channel creation logic when needed.
|
|
267
|
-
*/
|
|
268
|
-
export async function attemptConnection(
|
|
269
|
-
state: PeerConnectionState,
|
|
270
|
-
options: WebRTCConnectionOptions,
|
|
271
|
-
retryCount: number,
|
|
272
|
-
currentUseStun: boolean,
|
|
273
|
-
delay: number,
|
|
274
|
-
INITIAL_RETRY_DELAY: number,
|
|
275
|
-
CONNECTION_TIMEOUT: number,
|
|
276
|
-
MAX_RETRY_DELAY: number,
|
|
277
|
-
callbacks: PeerConnectionCallbacks
|
|
278
|
-
): Promise<any> {
|
|
279
|
-
if (state.isShuttingDown) {
|
|
280
|
-
throw new Error('Connection attempt cancelled - shutting down');
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
return new Promise((resolve, reject) => {
|
|
284
|
-
let connectionEstablished = false;
|
|
285
|
-
let timeoutId: NodeJS.Timeout | null = null;
|
|
286
|
-
let localConnection: RTCDataChannel | MediaStream | null = null;
|
|
287
|
-
const connectionId = ++state.currentConnectionId;
|
|
288
|
-
const isInitiator = state.isInitiator ?? options.isInitiator ?? true;
|
|
289
|
-
const connectionType =
|
|
290
|
-
state.connectionType ||
|
|
291
|
-
(options.mediaOptions?.isMediaConnection ? 'mediastream' : 'datachannel');
|
|
292
|
-
|
|
293
|
-
// Create message handler for WebRTC signaling
|
|
294
|
-
const messageHandler = async (message: any) => {
|
|
295
|
-
try {
|
|
296
|
-
if (state.isShuttingDown) {
|
|
297
|
-
return; // Don't process messages if shutting down
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
const channelId = `${state.channelPrefix}-${state.targetTwinId}`;
|
|
301
|
-
const messageType = message.data?.type;
|
|
302
|
-
|
|
303
|
-
switch (messageType) {
|
|
304
|
-
case `${channelId}:answer`:
|
|
305
|
-
if (isInitiator && state.pc) {
|
|
306
|
-
callbacks.reportConnectionState(
|
|
307
|
-
'Received answer, current signaling state: ' + state.pc.signalingState
|
|
308
|
-
);
|
|
309
|
-
if (state.pc.signalingState === 'have-local-offer') {
|
|
310
|
-
await state.pc.setRemoteDescription(message.data.data);
|
|
311
|
-
callbacks.reportConnectionState('Set remote description successfully');
|
|
312
|
-
|
|
313
|
-
// Apply any pending ICE candidates now that we have a remote description
|
|
314
|
-
if (state.pendingCandidates.length > 0) {
|
|
315
|
-
callbacks.reportConnectionState(
|
|
316
|
-
`Applying ${state.pendingCandidates.length} pending ICE candidates after answer`
|
|
317
|
-
);
|
|
318
|
-
for (const candidate of state.pendingCandidates) {
|
|
319
|
-
try {
|
|
320
|
-
await state.pc.addIceCandidate(candidate);
|
|
321
|
-
} catch (err) {
|
|
322
|
-
callbacks.reportError('Error applying pending ICE candidate:', err);
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
state.pendingCandidates = [];
|
|
326
|
-
}
|
|
327
|
-
} else {
|
|
328
|
-
callbacks.reportConnectionState(
|
|
329
|
-
`Ignoring answer - wrong state: ${state.pc.signalingState}`
|
|
330
|
-
);
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
break;
|
|
334
|
-
|
|
335
|
-
case `${channelId}:offer`:
|
|
336
|
-
if (!isInitiator && state.pc) {
|
|
337
|
-
callbacks.reportConnectionState(
|
|
338
|
-
'Received offer, current signaling state: ' + state.pc.signalingState
|
|
339
|
-
);
|
|
340
|
-
|
|
341
|
-
// Only process the offer if we're in a stable state or have-remote-offer
|
|
342
|
-
if (
|
|
343
|
-
state.pc.signalingState === 'stable' ||
|
|
344
|
-
state.pc.signalingState === 'have-remote-offer'
|
|
345
|
-
) {
|
|
346
|
-
// If we already have a remote offer, we need to rollback first
|
|
347
|
-
if (state.pc.signalingState === 'have-remote-offer') {
|
|
348
|
-
callbacks.reportConnectionState('Rolling back previous offer');
|
|
349
|
-
await state.pc.setLocalDescription({ type: 'rollback' });
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
await state.pc.setRemoteDescription(message.data.data);
|
|
353
|
-
callbacks.reportConnectionState('Set remote description successfully');
|
|
354
|
-
|
|
355
|
-
callbacks.reportConnectionState('Creating answer...');
|
|
356
|
-
const answer = await state.pc.createAnswer();
|
|
357
|
-
await state.pc.setLocalDescription(answer);
|
|
358
|
-
|
|
359
|
-
await callbacks.sendSignalingMessage('answer', answer);
|
|
360
|
-
|
|
361
|
-
// Apply any pending ICE candidates now that we have a remote description
|
|
362
|
-
if (state.pendingCandidates.length > 0) {
|
|
363
|
-
callbacks.reportConnectionState(
|
|
364
|
-
`Applying ${state.pendingCandidates.length} pending ICE candidates after offer`
|
|
365
|
-
);
|
|
366
|
-
for (const candidate of state.pendingCandidates) {
|
|
367
|
-
try {
|
|
368
|
-
await state.pc.addIceCandidate(candidate);
|
|
369
|
-
} catch (err) {
|
|
370
|
-
callbacks.reportError('Error applying pending ICE candidate:', err);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
state.pendingCandidates = [];
|
|
374
|
-
}
|
|
375
|
-
} else {
|
|
376
|
-
callbacks.reportConnectionState(
|
|
377
|
-
`Ignoring offer - wrong state: ${state.pc.signalingState}`
|
|
378
|
-
);
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
break;
|
|
382
|
-
|
|
383
|
-
case `${channelId}:ice`:
|
|
384
|
-
if (state.pc) {
|
|
385
|
-
if (state.pc.remoteDescription) {
|
|
386
|
-
try {
|
|
387
|
-
await state.pc.addIceCandidate(message.data.data);
|
|
388
|
-
} catch (err) {
|
|
389
|
-
callbacks.reportError('Error adding ICE candidate:', err);
|
|
390
|
-
}
|
|
391
|
-
} else {
|
|
392
|
-
callbacks.reportConnectionState('Storing ICE candidate for later');
|
|
393
|
-
state.pendingCandidates.push(message.data.data);
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
break;
|
|
397
|
-
}
|
|
398
|
-
} catch (error) {
|
|
399
|
-
callbacks.reportError('Error handling WebRTC message:', error);
|
|
400
|
-
}
|
|
401
|
-
};
|
|
402
|
-
|
|
403
|
-
const cleanup = () => {
|
|
404
|
-
callbacks.reportConnectionState('Running cleanup...');
|
|
405
|
-
if (timeoutId) clearTimeout(timeoutId);
|
|
406
|
-
callbacks.removeMessageHandling(messageHandler);
|
|
407
|
-
if (localConnection instanceof RTCDataChannel) {
|
|
408
|
-
localConnection.onopen = null;
|
|
409
|
-
localConnection.onclose = null;
|
|
410
|
-
localConnection.onerror = null;
|
|
411
|
-
localConnection.close();
|
|
412
|
-
} else if (localConnection instanceof MediaStream) {
|
|
413
|
-
localConnection.getTracks().forEach(track => track.stop());
|
|
414
|
-
}
|
|
415
|
-
if (state.pc) {
|
|
416
|
-
state.pc.onicecandidate = null;
|
|
417
|
-
state.pc.onconnectionstatechange = null;
|
|
418
|
-
state.pc.oniceconnectionstatechange = null;
|
|
419
|
-
try {
|
|
420
|
-
(state.pc as RTCPeerConnection).close();
|
|
421
|
-
} catch (err) {
|
|
422
|
-
callbacks.reportError('Error closing peer connection', err);
|
|
423
|
-
}
|
|
424
|
-
state.pc = null;
|
|
425
|
-
}
|
|
426
|
-
state.pendingCandidates = [];
|
|
427
|
-
state.activeDataChannel = null;
|
|
428
|
-
state.activeMediaStream = null;
|
|
429
|
-
|
|
430
|
-
// Also do any local cleanup
|
|
431
|
-
callbacks.onCloseCleanup();
|
|
432
|
-
};
|
|
433
|
-
|
|
434
|
-
const safeReject = (error: Error) => {
|
|
435
|
-
cleanup();
|
|
436
|
-
reject(error);
|
|
437
|
-
};
|
|
438
|
-
|
|
439
|
-
const retryConnection = () => {
|
|
440
|
-
if (state.isShuttingDown) {
|
|
441
|
-
safeReject(new Error('Connection attempt cancelled - shutting down'));
|
|
442
|
-
return;
|
|
443
|
-
}
|
|
444
|
-
callbacks.reportConnectionState(
|
|
445
|
-
`Connection attempt ${retryCount + 1} failed ${currentUseStun ? 'with' : 'without'} STUN`
|
|
446
|
-
);
|
|
447
|
-
cleanup();
|
|
448
|
-
|
|
449
|
-
// The original code manipulates STUN usage on first retry,
|
|
450
|
-
// then uses exponential backoff.
|
|
451
|
-
if (retryCount === 0) {
|
|
452
|
-
callbacks.reportConnectionState(
|
|
453
|
-
`Retrying with ${!currentUseStun ? 'STUN' : 'direct'} connection...`
|
|
454
|
-
);
|
|
455
|
-
callbacks
|
|
456
|
-
.retryConnection(retryCount + 1, !currentUseStun)
|
|
457
|
-
.then(res => resolve(res))
|
|
458
|
-
.catch(err => reject(err));
|
|
459
|
-
return;
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
// Otherwise, wait and retry with original STUN setting
|
|
463
|
-
callbacks.reportConnectionState(
|
|
464
|
-
`Retrying in ${Math.floor(delay / 1000)} seconds... (attempt ${retryCount + 1})`
|
|
465
|
-
);
|
|
466
|
-
setTimeout(() => {
|
|
467
|
-
const nextDelay = Math.min(MAX_RETRY_DELAY, INITIAL_RETRY_DELAY * Math.pow(2, retryCount));
|
|
468
|
-
callbacks
|
|
469
|
-
.retryConnection(retryCount + 1, currentUseStun, Math.min(nextDelay, MAX_RETRY_DELAY))
|
|
470
|
-
.then(res => resolve(res))
|
|
471
|
-
.catch(err => reject(err));
|
|
472
|
-
}, delay);
|
|
473
|
-
};
|
|
474
|
-
|
|
475
|
-
const initConnection = async () => {
|
|
476
|
-
if (typeof RTCPeerConnection === 'undefined') {
|
|
477
|
-
callbacks.reportError(
|
|
478
|
-
'RTCPeerConnection is not available. WebRTC globals initialization may have failed.'
|
|
479
|
-
);
|
|
480
|
-
throw new Error(
|
|
481
|
-
'RTCPeerConnection is not available. Cannot proceed with WebRTC connection.'
|
|
482
|
-
);
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
// Set up message handling for signaling
|
|
486
|
-
callbacks.setupMessageHandling(messageHandler);
|
|
487
|
-
|
|
488
|
-
// Original code: await subscribeTwin(targetTwinId)
|
|
489
|
-
// delegated to callback
|
|
490
|
-
await callbacks.onSubscribe();
|
|
491
|
-
|
|
492
|
-
callbacks.reportConnectionState(
|
|
493
|
-
`Attempting data connection ${currentUseStun ? 'with' : 'without'} STUN servers...`
|
|
494
|
-
);
|
|
495
|
-
state.pc = createPeerConnection(currentUseStun);
|
|
496
|
-
const pc = state.pc!;
|
|
497
|
-
|
|
498
|
-
// Set up ICE handling
|
|
499
|
-
handleIceAndConnectionState(state, pc, callbacks, connectionId);
|
|
500
|
-
|
|
501
|
-
// Defer connection creation to original file (keeping specific code there)
|
|
502
|
-
try {
|
|
503
|
-
localConnection = await callbacks.onCreateConnection(pc, connectionId, isInitiator);
|
|
504
|
-
|
|
505
|
-
if (localConnection instanceof RTCDataChannel) {
|
|
506
|
-
state.activeDataChannel = localConnection;
|
|
507
|
-
} else if (localConnection instanceof MediaStream) {
|
|
508
|
-
state.activeMediaStream = localConnection;
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
// Monitor connection state changes
|
|
512
|
-
pc.onconnectionstatechange = () => {
|
|
513
|
-
if (!pc) return;
|
|
514
|
-
callbacks.reportConnectionState(`Connection state: ${pc.connectionState}`);
|
|
515
|
-
if (pc.connectionState === 'connected') {
|
|
516
|
-
connectionEstablished = true;
|
|
517
|
-
if (timeoutId) clearTimeout(timeoutId);
|
|
518
|
-
|
|
519
|
-
// For MediaStreams, we need to wait for tracks to be received
|
|
520
|
-
if (connectionType === 'mediastream') {
|
|
521
|
-
// Check if we have received any tracks
|
|
522
|
-
const hasTracks = pc.getReceivers().some(receiver => receiver.track);
|
|
523
|
-
if (!hasTracks) {
|
|
524
|
-
callbacks.reportConnectionState('Connected but no tracks received yet');
|
|
525
|
-
return;
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
// If we have an active connection, resolve the promise
|
|
530
|
-
if (connectionEstablished && !state.isShuttingDown) {
|
|
531
|
-
const result = {
|
|
532
|
-
_rawPeerConnection: pc,
|
|
533
|
-
close: () => {
|
|
534
|
-
state.isShuttingDown = true;
|
|
535
|
-
cleanup();
|
|
536
|
-
},
|
|
537
|
-
};
|
|
538
|
-
|
|
539
|
-
// Add connection-specific properties
|
|
540
|
-
if (connectionType === 'datachannel') {
|
|
541
|
-
Object.assign(result, {
|
|
542
|
-
dataChannel: {
|
|
543
|
-
send: () => {}, // This will be replaced by the persistent channel
|
|
544
|
-
onMessage: () => {}, // This will be replaced by the persistent channel
|
|
545
|
-
offMessage: () => {}, // This will be replaced by the persistent channel
|
|
546
|
-
onClose: () => {}, // This will be replaced by the persistent channel
|
|
547
|
-
offClose: () => {}, // This will be replaced by the persistent channel
|
|
548
|
-
close: () => {
|
|
549
|
-
state.isShuttingDown = true;
|
|
550
|
-
cleanup();
|
|
551
|
-
},
|
|
552
|
-
},
|
|
553
|
-
_rawDataChannel: state.activeDataChannel,
|
|
554
|
-
});
|
|
555
|
-
} else if (connectionType === 'mediastream') {
|
|
556
|
-
Object.assign(result, {
|
|
557
|
-
mediaStream: {
|
|
558
|
-
getTracks: () => [], // This will be replaced by the persistent media stream
|
|
559
|
-
onTrack: () => {}, // This will be replaced by the persistent media stream
|
|
560
|
-
offTrack: () => {}, // This will be replaced by the persistent media stream
|
|
561
|
-
onClose: () => {}, // This will be replaced by the persistent media stream
|
|
562
|
-
offClose: () => {}, // This will be replaced by the persistent media stream
|
|
563
|
-
close: () => {
|
|
564
|
-
state.isShuttingDown = true;
|
|
565
|
-
cleanup();
|
|
566
|
-
},
|
|
567
|
-
addTrack: () => {}, // This will be replaced by the persistent media stream
|
|
568
|
-
},
|
|
569
|
-
_rawMediaStream: state.activeMediaStream,
|
|
570
|
-
});
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
resolve(result);
|
|
574
|
-
}
|
|
575
|
-
} else if (
|
|
576
|
-
pc.connectionState === 'failed' ||
|
|
577
|
-
pc.connectionState === 'disconnected' ||
|
|
578
|
-
pc.connectionState === 'closed'
|
|
579
|
-
) {
|
|
580
|
-
if (connectionEstablished) {
|
|
581
|
-
callbacks.reportConnectionState('Connection lost, initiating retry...');
|
|
582
|
-
retryConnection();
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
};
|
|
586
|
-
|
|
587
|
-
// Set up ICE connection state monitoring
|
|
588
|
-
pc.oniceconnectionstatechange = () => {
|
|
589
|
-
if (!pc) return;
|
|
590
|
-
callbacks.reportConnectionState(`ICE connection state: ${pc.iceConnectionState}`);
|
|
591
|
-
if (pc.iceConnectionState === 'failed' && connectionEstablished) {
|
|
592
|
-
callbacks.reportConnectionState('ICE Connection failed, initiating retry...');
|
|
593
|
-
retryConnection();
|
|
594
|
-
}
|
|
595
|
-
};
|
|
596
|
-
|
|
597
|
-
// Create and send offer for initiator
|
|
598
|
-
if (isInitiator) {
|
|
599
|
-
try {
|
|
600
|
-
callbacks.reportConnectionState('Creating offer...');
|
|
601
|
-
const offer = await pc.createOffer();
|
|
602
|
-
callbacks.reportConnectionState('Setting local description...');
|
|
603
|
-
await pc.setLocalDescription(offer);
|
|
604
|
-
callbacks.reportConnectionState('Sending offer...');
|
|
605
|
-
await callbacks.sendSignalingMessage('offer', offer);
|
|
606
|
-
} catch (error) {
|
|
607
|
-
callbacks.reportError('Error creating/sending offer:', error);
|
|
608
|
-
retryConnection();
|
|
609
|
-
return;
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
// Timeout to detect initial connection failure
|
|
614
|
-
timeoutId = setTimeout(() => {
|
|
615
|
-
if (!connectionEstablished) {
|
|
616
|
-
callbacks.reportConnectionState(
|
|
617
|
-
`Connection attempt timed out ${currentUseStun ? 'with' : 'without'} STUN`
|
|
618
|
-
);
|
|
619
|
-
retryConnection();
|
|
620
|
-
}
|
|
621
|
-
}, CONNECTION_TIMEOUT);
|
|
622
|
-
} catch (error) {
|
|
623
|
-
callbacks.reportError('Error creating connection in callback', error);
|
|
624
|
-
retryConnection();
|
|
625
|
-
return;
|
|
626
|
-
}
|
|
627
|
-
};
|
|
628
|
-
|
|
629
|
-
// Start everything
|
|
630
|
-
initConnection().catch(error => {
|
|
631
|
-
callbacks.reportError('Unhandled error in connection attempt', error);
|
|
632
|
-
safeReject(error);
|
|
633
|
-
});
|
|
634
|
-
});
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
/**
|
|
638
|
-
* The reconnect trigger logic, abstracted here.
|
|
639
|
-
*/
|
|
640
|
-
export function triggerReconnect(
|
|
641
|
-
state: PeerConnectionState,
|
|
642
|
-
attempt: number,
|
|
643
|
-
RECONNECT_DELAY: number,
|
|
644
|
-
callbacks: PeerConnectionCallbacks
|
|
645
|
-
) {
|
|
646
|
-
// These checks are now handled in the webrtc-datachannel.ts file
|
|
647
|
-
// to maintain consistent state between the two files
|
|
648
|
-
|
|
649
|
-
state.currentConnectionId++;
|
|
650
|
-
callbacks.reportConnectionState(`Starting reconnection process (attempt ${attempt})`);
|
|
651
|
-
|
|
652
|
-
// Force close any active data channel
|
|
653
|
-
if (state.activeDataChannel && typeof state.activeDataChannel.close === 'function') {
|
|
654
|
-
try {
|
|
655
|
-
state.activeDataChannel.close();
|
|
656
|
-
} catch (err) {
|
|
657
|
-
callbacks.reportError('Error closing active data channel:', err);
|
|
658
|
-
}
|
|
659
|
-
state.activeDataChannel = null;
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
// Clean up any existing peer connection
|
|
663
|
-
if (state.pc) {
|
|
664
|
-
try {
|
|
665
|
-
(state.pc as RTCPeerConnection).close();
|
|
666
|
-
} catch (err) {
|
|
667
|
-
callbacks.reportError('Error closing old peer connection:', err);
|
|
668
|
-
}
|
|
669
|
-
state.pc = null;
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
const reconnectAttempt = () => {
|
|
673
|
-
// The original code tries with toggled useStun or direct
|
|
674
|
-
callbacks
|
|
675
|
-
.retryConnection(0, !(state.useStun ?? true))
|
|
676
|
-
.then(() => {
|
|
677
|
-
callbacks.reportConnectionState(`Reconnection successful (attempt ${attempt})`);
|
|
678
|
-
})
|
|
679
|
-
.catch(error => {
|
|
680
|
-
callbacks.reportError(`Reconnection failed (attempt ${attempt})`, error);
|
|
681
|
-
setTimeout(
|
|
682
|
-
reconnectAttempt,
|
|
683
|
-
Math.min(100 * Math.pow(2, attempt), 5000) // backoff
|
|
684
|
-
);
|
|
685
|
-
});
|
|
686
|
-
};
|
|
687
|
-
|
|
688
|
-
setTimeout(reconnectAttempt, RECONNECT_DELAY);
|
|
689
|
-
}
|