@phystack/hub-client 4.5.19-dev → 4.5.21-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,494 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebRTC Comprehensive Test Suite
|
|
3
|
+
*
|
|
4
|
+
* Tests the following scenarios:
|
|
5
|
+
* 1. DataChannel - default (unnamed) channel
|
|
6
|
+
* 2. DataChannel - named channels (multiple to same peer)
|
|
7
|
+
* 3. DataChannel - reconnection with handler persistence
|
|
8
|
+
* 4. MediaStream - connection establishment
|
|
9
|
+
* 5. MediaStream - named channels
|
|
10
|
+
*
|
|
11
|
+
* Usage (two terminals):
|
|
12
|
+
*
|
|
13
|
+
* Terminal 1 (initiator):
|
|
14
|
+
* ```
|
|
15
|
+
* PHYHUB_DIRECT=true \
|
|
16
|
+
* DEVICE_ID=<device-a-id> \
|
|
17
|
+
* ACCESS_KEY=<device-a-key> \
|
|
18
|
+
* PEER_TWIN_ID=<device-b-twin-id> \
|
|
19
|
+
* ROLE=initiator \
|
|
20
|
+
* npx ts-node src/test/webrtc-comprehensive-test.ts
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* Terminal 2 (responder):
|
|
24
|
+
* ```
|
|
25
|
+
* PHYHUB_DIRECT=true \
|
|
26
|
+
* DEVICE_ID=<device-b-id> \
|
|
27
|
+
* ACCESS_KEY=<device-b-key> \
|
|
28
|
+
* PEER_TWIN_ID=<device-a-twin-id> \
|
|
29
|
+
* ROLE=responder \
|
|
30
|
+
* npx ts-node src/test/webrtc-comprehensive-test.ts
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { PhyHubClient } from '../index';
|
|
35
|
+
import { PhygridDataChannel } from '../services/webrtc/types';
|
|
36
|
+
|
|
37
|
+
interface TestResult {
|
|
38
|
+
name: string;
|
|
39
|
+
passed: boolean;
|
|
40
|
+
details: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const MESSAGES_PER_TEST = 3;
|
|
44
|
+
const results: TestResult[] = [];
|
|
45
|
+
|
|
46
|
+
function log(msg: string): void {
|
|
47
|
+
console.log(`[${new Date().toISOString().split('T')[1].slice(0, 8)}] ${msg}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function sleep(ms: number): Promise<void> {
|
|
51
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function addResult(name: string, passed: boolean, details: string): void {
|
|
55
|
+
results.push({ name, passed, details });
|
|
56
|
+
const status = passed ? '\x1b[32mPASS\x1b[0m' : '\x1b[31mFAIL\x1b[0m';
|
|
57
|
+
log(`[${status}] ${name}: ${details}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// =============================================================================
|
|
61
|
+
// Test 1: DataChannel Default (Unnamed)
|
|
62
|
+
// =============================================================================
|
|
63
|
+
async function testDataChannelDefault(
|
|
64
|
+
client: PhyHubClient,
|
|
65
|
+
peerTwinId: string,
|
|
66
|
+
isInitiator: boolean
|
|
67
|
+
): Promise<void> {
|
|
68
|
+
log('\n=== Test 1: DataChannel Default ===');
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
let channel: PhygridDataChannel;
|
|
72
|
+
let receivedCount = 0;
|
|
73
|
+
|
|
74
|
+
if (isInitiator) {
|
|
75
|
+
channel = await client.getDataChannel(peerTwinId);
|
|
76
|
+
log(`Created default channel. Name: ${channel.getChannelName()}`);
|
|
77
|
+
|
|
78
|
+
if (channel.getChannelName() !== 'default') {
|
|
79
|
+
throw new Error(`Expected channel name 'default', got '${channel.getChannelName()}'`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
channel.onMessage(() => receivedCount++);
|
|
83
|
+
await sleep(2000);
|
|
84
|
+
|
|
85
|
+
for (let i = 0; i < MESSAGES_PER_TEST; i++) {
|
|
86
|
+
channel.send({ test: 'default', num: i });
|
|
87
|
+
await sleep(200);
|
|
88
|
+
}
|
|
89
|
+
await sleep(2000);
|
|
90
|
+
|
|
91
|
+
addResult('DataChannel Default', receivedCount >= MESSAGES_PER_TEST,
|
|
92
|
+
`Sent ${MESSAGES_PER_TEST}, received ${receivedCount}`);
|
|
93
|
+
} else {
|
|
94
|
+
await new Promise<void>((resolve, reject) => {
|
|
95
|
+
const timeout = setTimeout(() => reject(new Error('Timeout')), 30000);
|
|
96
|
+
|
|
97
|
+
client.onDataChannel(peerTwinId, (ch) => {
|
|
98
|
+
clearTimeout(timeout);
|
|
99
|
+
channel = ch;
|
|
100
|
+
log(`Received default channel. Name: ${ch.getChannelName()}`);
|
|
101
|
+
|
|
102
|
+
ch.onMessage((data) => {
|
|
103
|
+
receivedCount++;
|
|
104
|
+
ch.send({ echo: data });
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Wait for messages then resolve
|
|
108
|
+
setTimeout(() => {
|
|
109
|
+
addResult('DataChannel Default', receivedCount >= MESSAGES_PER_TEST,
|
|
110
|
+
`Received ${receivedCount} messages`);
|
|
111
|
+
resolve();
|
|
112
|
+
}, 8000);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
} catch (error: any) {
|
|
117
|
+
addResult('DataChannel Default', false, error.message);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// =============================================================================
|
|
122
|
+
// Test 2: DataChannel Named Channels
|
|
123
|
+
// =============================================================================
|
|
124
|
+
async function testDataChannelNamed(
|
|
125
|
+
client: PhyHubClient,
|
|
126
|
+
peerTwinId: string,
|
|
127
|
+
isInitiator: boolean
|
|
128
|
+
): Promise<void> {
|
|
129
|
+
log('\n=== Test 2: DataChannel Named Channels ===');
|
|
130
|
+
|
|
131
|
+
const channelNames = ['control', 'data', 'telemetry'];
|
|
132
|
+
const receivedByChannel: Map<string, number> = new Map();
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
if (isInitiator) {
|
|
136
|
+
const channels: PhygridDataChannel[] = [];
|
|
137
|
+
|
|
138
|
+
// Create all named channels
|
|
139
|
+
for (const name of channelNames) {
|
|
140
|
+
const ch = await client.getDataChannel(peerTwinId, name);
|
|
141
|
+
log(`Created channel '${name}'. Reported name: ${ch.getChannelName()}`);
|
|
142
|
+
|
|
143
|
+
if (ch.getChannelName() !== name) {
|
|
144
|
+
throw new Error(`Expected channel name '${name}', got '${ch.getChannelName()}'`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
receivedByChannel.set(name, 0);
|
|
148
|
+
ch.onMessage(() => {
|
|
149
|
+
receivedByChannel.set(name, (receivedByChannel.get(name) || 0) + 1);
|
|
150
|
+
});
|
|
151
|
+
channels.push(ch);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
await sleep(3000);
|
|
155
|
+
|
|
156
|
+
// Send messages on each channel
|
|
157
|
+
for (let i = 0; i < channels.length; i++) {
|
|
158
|
+
const ch = channels[i];
|
|
159
|
+
for (let j = 0; j < MESSAGES_PER_TEST; j++) {
|
|
160
|
+
ch.send({ channel: channelNames[i], num: j });
|
|
161
|
+
await sleep(100);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
await sleep(3000);
|
|
166
|
+
|
|
167
|
+
// Check results
|
|
168
|
+
let allPassed = true;
|
|
169
|
+
for (const name of channelNames) {
|
|
170
|
+
const count = receivedByChannel.get(name) || 0;
|
|
171
|
+
if (count < MESSAGES_PER_TEST) allPassed = false;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const details = channelNames.map(n => `${n}:${receivedByChannel.get(n)}`).join(', ');
|
|
175
|
+
addResult('DataChannel Named', allPassed, details);
|
|
176
|
+
|
|
177
|
+
channels.forEach(ch => ch.close());
|
|
178
|
+
} else {
|
|
179
|
+
// Responder: accept all named channels
|
|
180
|
+
const promises = channelNames.map(name => new Promise<void>((resolve, reject) => {
|
|
181
|
+
const timeout = setTimeout(() => reject(new Error(`Timeout for ${name}`)), 30000);
|
|
182
|
+
receivedByChannel.set(name, 0);
|
|
183
|
+
|
|
184
|
+
client.onDataChannel(peerTwinId, (ch) => {
|
|
185
|
+
clearTimeout(timeout);
|
|
186
|
+
log(`Received channel '${name}'. Reported: ${ch.getChannelName()}`);
|
|
187
|
+
|
|
188
|
+
ch.onMessage((data) => {
|
|
189
|
+
receivedByChannel.set(name, (receivedByChannel.get(name) || 0) + 1);
|
|
190
|
+
ch.send({ echo: data });
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
setTimeout(resolve, 10000);
|
|
194
|
+
}, name);
|
|
195
|
+
}));
|
|
196
|
+
|
|
197
|
+
await Promise.all(promises);
|
|
198
|
+
|
|
199
|
+
const details = channelNames.map(n => `${n}:${receivedByChannel.get(n)}`).join(', ');
|
|
200
|
+
const allPassed = channelNames.every(n => (receivedByChannel.get(n) || 0) >= MESSAGES_PER_TEST);
|
|
201
|
+
addResult('DataChannel Named', allPassed, details);
|
|
202
|
+
}
|
|
203
|
+
} catch (error: any) {
|
|
204
|
+
addResult('DataChannel Named', false, error.message);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// =============================================================================
|
|
209
|
+
// Test 3: DataChannel Reconnection
|
|
210
|
+
// =============================================================================
|
|
211
|
+
async function testDataChannelReconnection(
|
|
212
|
+
client: PhyHubClient,
|
|
213
|
+
peerTwinId: string,
|
|
214
|
+
isInitiator: boolean
|
|
215
|
+
): Promise<void> {
|
|
216
|
+
log('\n=== Test 3: DataChannel Reconnection ===');
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
const manager = await client.getWebRTCManager({ verbose: true });
|
|
220
|
+
let reconnectDetected = false;
|
|
221
|
+
let messagesAfterReconnect = 0;
|
|
222
|
+
let channel: PhygridDataChannel;
|
|
223
|
+
|
|
224
|
+
manager.on('reconnecting', () => {
|
|
225
|
+
log('[EVENT] Reconnecting...');
|
|
226
|
+
});
|
|
227
|
+
manager.on('reconnected', () => {
|
|
228
|
+
log('[EVENT] Reconnected!');
|
|
229
|
+
reconnectDetected = true;
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
if (isInitiator) {
|
|
233
|
+
channel = await client.getDataChannel(peerTwinId, 'reconnect-test');
|
|
234
|
+
log('Channel created for reconnect test');
|
|
235
|
+
|
|
236
|
+
// Handler registered ONCE - should persist across reconnect
|
|
237
|
+
channel.onMessage(() => messagesAfterReconnect++);
|
|
238
|
+
|
|
239
|
+
// Send initial messages
|
|
240
|
+
for (let i = 0; i < 3; i++) {
|
|
241
|
+
channel.send({ phase: 'before', num: i });
|
|
242
|
+
await sleep(200);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
log('Waiting 5s before simulating disconnect...');
|
|
246
|
+
await sleep(5000);
|
|
247
|
+
|
|
248
|
+
// Simulate disconnect by requesting it from responder
|
|
249
|
+
channel.send({ command: 'simulate-disconnect' });
|
|
250
|
+
|
|
251
|
+
log('Waiting 15s for reconnection...');
|
|
252
|
+
await sleep(15000);
|
|
253
|
+
|
|
254
|
+
// Send messages after reconnection
|
|
255
|
+
for (let i = 0; i < MESSAGES_PER_TEST; i++) {
|
|
256
|
+
channel.send({ phase: 'after', num: i });
|
|
257
|
+
await sleep(200);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
await sleep(3000);
|
|
261
|
+
|
|
262
|
+
const passed = messagesAfterReconnect >= MESSAGES_PER_TEST;
|
|
263
|
+
addResult('DataChannel Reconnection', passed,
|
|
264
|
+
`Reconnect detected: ${reconnectDetected}, messages after: ${messagesAfterReconnect}`);
|
|
265
|
+
} else {
|
|
266
|
+
await new Promise<void>((resolve, reject) => {
|
|
267
|
+
const timeout = setTimeout(() => reject(new Error('Timeout')), 60000);
|
|
268
|
+
|
|
269
|
+
client.onDataChannel(peerTwinId, (ch) => {
|
|
270
|
+
clearTimeout(timeout);
|
|
271
|
+
channel = ch;
|
|
272
|
+
log('Reconnect test channel received');
|
|
273
|
+
|
|
274
|
+
// Handler registered ONCE
|
|
275
|
+
ch.onMessage((data) => {
|
|
276
|
+
messagesAfterReconnect++;
|
|
277
|
+
ch.send({ echo: data });
|
|
278
|
+
|
|
279
|
+
if (data.command === 'simulate-disconnect') {
|
|
280
|
+
log('Simulating disconnect...');
|
|
281
|
+
// Note: In a real test, we would close the underlying connection
|
|
282
|
+
// For now, we just acknowledge the command
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
setTimeout(() => {
|
|
287
|
+
addResult('DataChannel Reconnection', messagesAfterReconnect > 0,
|
|
288
|
+
`Received ${messagesAfterReconnect} total messages`);
|
|
289
|
+
resolve();
|
|
290
|
+
}, 30000);
|
|
291
|
+
}, 'reconnect-test');
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
} catch (error: any) {
|
|
295
|
+
addResult('DataChannel Reconnection', false, error.message);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// =============================================================================
|
|
300
|
+
// Test 4: MediaStream Connection
|
|
301
|
+
// =============================================================================
|
|
302
|
+
async function testMediaStreamConnection(
|
|
303
|
+
client: PhyHubClient,
|
|
304
|
+
peerTwinId: string,
|
|
305
|
+
isInitiator: boolean
|
|
306
|
+
): Promise<void> {
|
|
307
|
+
log('\n=== Test 4: MediaStream Connection ===');
|
|
308
|
+
|
|
309
|
+
try {
|
|
310
|
+
if (isInitiator) {
|
|
311
|
+
const { stream, close } = await client.getMediaStream(peerTwinId);
|
|
312
|
+
log(`MediaStream created. Target: ${stream.getTargetTwinId()}, Channel: ${stream.getChannelName()}`);
|
|
313
|
+
|
|
314
|
+
if (stream.getChannelName() !== 'default') {
|
|
315
|
+
throw new Error(`Expected channel name 'default', got '${stream.getChannelName()}'`);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Track listener
|
|
319
|
+
stream.onTrack((track) => {
|
|
320
|
+
log(`Received track: ${track.kind}`);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
await sleep(5000);
|
|
324
|
+
|
|
325
|
+
addResult('MediaStream Connection', true,
|
|
326
|
+
`Connected to ${stream.getTargetTwinId()}`);
|
|
327
|
+
|
|
328
|
+
close();
|
|
329
|
+
} else {
|
|
330
|
+
await new Promise<void>((resolve, reject) => {
|
|
331
|
+
const timeout = setTimeout(() => reject(new Error('Timeout')), 30000);
|
|
332
|
+
|
|
333
|
+
client.onMediaStream(peerTwinId, (stream) => {
|
|
334
|
+
clearTimeout(timeout);
|
|
335
|
+
log(`MediaStream received. Channel: ${stream.getChannelName()}`);
|
|
336
|
+
|
|
337
|
+
stream.onTrack((track) => {
|
|
338
|
+
log(`Track received: ${track.kind}`);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
setTimeout(() => {
|
|
342
|
+
addResult('MediaStream Connection', true,
|
|
343
|
+
`Connected from ${stream.getTargetTwinId()}`);
|
|
344
|
+
resolve();
|
|
345
|
+
}, 5000);
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
} catch (error: any) {
|
|
350
|
+
addResult('MediaStream Connection', false, error.message);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// =============================================================================
|
|
355
|
+
// Test 5: MediaStream Named Channels
|
|
356
|
+
// =============================================================================
|
|
357
|
+
async function testMediaStreamNamed(
|
|
358
|
+
client: PhyHubClient,
|
|
359
|
+
peerTwinId: string,
|
|
360
|
+
isInitiator: boolean
|
|
361
|
+
): Promise<void> {
|
|
362
|
+
log('\n=== Test 5: MediaStream Named Channels ===');
|
|
363
|
+
|
|
364
|
+
const channelNames = ['video', 'audio'];
|
|
365
|
+
|
|
366
|
+
try {
|
|
367
|
+
if (isInitiator) {
|
|
368
|
+
const streams: Array<{ stream: any; close: () => void }> = [];
|
|
369
|
+
|
|
370
|
+
for (const name of channelNames) {
|
|
371
|
+
const result = await client.getMediaStream(peerTwinId, { channelName: name });
|
|
372
|
+
log(`Created MediaStream '${name}'. Reported: ${result.stream.getChannelName()}`);
|
|
373
|
+
|
|
374
|
+
if (result.stream.getChannelName() !== name) {
|
|
375
|
+
throw new Error(`Expected '${name}', got '${result.stream.getChannelName()}'`);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
streams.push(result);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
await sleep(5000);
|
|
382
|
+
|
|
383
|
+
addResult('MediaStream Named', true,
|
|
384
|
+
`Created ${channelNames.length} named streams`);
|
|
385
|
+
|
|
386
|
+
streams.forEach(s => s.close());
|
|
387
|
+
} else {
|
|
388
|
+
const receivedChannels: string[] = [];
|
|
389
|
+
|
|
390
|
+
const promises = channelNames.map(name => new Promise<void>((resolve, reject) => {
|
|
391
|
+
const timeout = setTimeout(() => reject(new Error(`Timeout for ${name}`)), 30000);
|
|
392
|
+
|
|
393
|
+
client.onMediaStream(peerTwinId, (stream) => {
|
|
394
|
+
clearTimeout(timeout);
|
|
395
|
+
log(`Received MediaStream '${name}'. Reported: ${stream.getChannelName()}`);
|
|
396
|
+
receivedChannels.push(stream.getChannelName());
|
|
397
|
+
setTimeout(resolve, 3000);
|
|
398
|
+
}, { channelName: name });
|
|
399
|
+
}));
|
|
400
|
+
|
|
401
|
+
await Promise.all(promises);
|
|
402
|
+
|
|
403
|
+
const passed = receivedChannels.length === channelNames.length;
|
|
404
|
+
addResult('MediaStream Named', passed,
|
|
405
|
+
`Received channels: ${receivedChannels.join(', ')}`);
|
|
406
|
+
}
|
|
407
|
+
} catch (error: any) {
|
|
408
|
+
addResult('MediaStream Named', false, error.message);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// =============================================================================
|
|
413
|
+
// Main
|
|
414
|
+
// =============================================================================
|
|
415
|
+
async function main(): Promise<void> {
|
|
416
|
+
console.log('='.repeat(60));
|
|
417
|
+
console.log('WebRTC Comprehensive Test Suite');
|
|
418
|
+
console.log('='.repeat(60));
|
|
419
|
+
|
|
420
|
+
if (process.env.PHYHUB_DIRECT !== 'true') {
|
|
421
|
+
console.error('ERROR: Set PHYHUB_DIRECT=true');
|
|
422
|
+
process.exit(1);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const peerTwinId = process.env.PEER_TWIN_ID;
|
|
426
|
+
const role = process.env.ROLE?.toLowerCase();
|
|
427
|
+
const testFilter = process.env.TEST; // Optional: run specific test
|
|
428
|
+
|
|
429
|
+
if (!peerTwinId) {
|
|
430
|
+
console.error('ERROR: Set PEER_TWIN_ID');
|
|
431
|
+
process.exit(1);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (role !== 'initiator' && role !== 'responder') {
|
|
435
|
+
console.error('ERROR: Set ROLE to initiator or responder');
|
|
436
|
+
process.exit(1);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const isInitiator = role === 'initiator';
|
|
440
|
+
log(`Role: ${role}`);
|
|
441
|
+
log(`Peer Twin ID: ${peerTwinId}`);
|
|
442
|
+
if (testFilter) log(`Running only: ${testFilter}`);
|
|
443
|
+
|
|
444
|
+
try {
|
|
445
|
+
log('\nConnecting to PhyHub...');
|
|
446
|
+
const client = await PhyHubClient.connect();
|
|
447
|
+
log('Connected!\n');
|
|
448
|
+
|
|
449
|
+
// Run tests based on filter
|
|
450
|
+
const tests = [
|
|
451
|
+
{ name: 'default', fn: testDataChannelDefault },
|
|
452
|
+
{ name: 'named', fn: testDataChannelNamed },
|
|
453
|
+
{ name: 'reconnect', fn: testDataChannelReconnection },
|
|
454
|
+
{ name: 'media', fn: testMediaStreamConnection },
|
|
455
|
+
{ name: 'media-named', fn: testMediaStreamNamed },
|
|
456
|
+
];
|
|
457
|
+
|
|
458
|
+
for (const test of tests) {
|
|
459
|
+
if (!testFilter || test.name === testFilter) {
|
|
460
|
+
await test.fn(client, peerTwinId, isInitiator);
|
|
461
|
+
await sleep(2000);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Print summary
|
|
466
|
+
console.log('\n' + '='.repeat(60));
|
|
467
|
+
console.log('TEST SUMMARY');
|
|
468
|
+
console.log('='.repeat(60));
|
|
469
|
+
console.log('Test'.padEnd(30) + 'Status');
|
|
470
|
+
console.log('-'.repeat(45));
|
|
471
|
+
|
|
472
|
+
let passed = 0;
|
|
473
|
+
let failed = 0;
|
|
474
|
+
for (const r of results) {
|
|
475
|
+
const status = r.passed ? '\x1b[32mPASS\x1b[0m' : '\x1b[31mFAIL\x1b[0m';
|
|
476
|
+
console.log(r.name.padEnd(30) + status);
|
|
477
|
+
if (r.passed) passed++;
|
|
478
|
+
else failed++;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
console.log('-'.repeat(45));
|
|
482
|
+
console.log(`Total: ${passed} passed, ${failed} failed`);
|
|
483
|
+
console.log('='.repeat(60));
|
|
484
|
+
|
|
485
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
486
|
+
} catch (error) {
|
|
487
|
+
console.error('\n[FATAL]', error);
|
|
488
|
+
process.exit(1);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (require.main === module) {
|
|
493
|
+
main();
|
|
494
|
+
}
|