@4players/odin-nodejs 0.10.3 → 0.11.1
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/CHANGELOG.md +72 -0
- package/LICENSE +21 -0
- package/README.md +603 -44
- package/binding.gyp +29 -13
- package/cppsrc/binding.cpp +3 -6
- package/cppsrc/odinbindings.cpp +9 -45
- package/cppsrc/odincipher.cpp +92 -0
- package/cppsrc/odincipher.h +32 -0
- package/cppsrc/odinclient.cpp +19 -158
- package/cppsrc/odinclient.h +2 -5
- package/cppsrc/odinmedia.cpp +144 -186
- package/cppsrc/odinmedia.h +51 -18
- package/cppsrc/odinroom.cpp +675 -635
- package/cppsrc/odinroom.h +76 -26
- package/cppsrc/utilities.cpp +11 -81
- package/cppsrc/utilities.h +25 -140
- package/index.cjs +829 -0
- package/index.d.ts +3 -4
- package/libs/bin/linux/arm64/libodin.so +0 -0
- package/libs/bin/linux/arm64/libodin_crypto.so +0 -0
- package/libs/bin/linux/ia32/libodin.so +0 -0
- package/libs/bin/linux/ia32/libodin_crypto.so +0 -0
- package/libs/bin/linux/x64/libodin.so +0 -0
- package/libs/bin/linux/x64/libodin_crypto.so +0 -0
- package/{prebuilds/darwin-x64/node.napi.node → libs/bin/macos/universal/libodin.dylib} +0 -0
- package/libs/bin/macos/universal/libodin_crypto.dylib +0 -0
- package/libs/bin/windows/arm64/odin.dll +0 -0
- package/libs/bin/windows/arm64/odin.lib +0 -0
- package/libs/bin/windows/arm64/odin_crypto.dll +0 -0
- package/libs/bin/windows/arm64/odin_crypto.lib +0 -0
- package/libs/bin/windows/ia32/odin.dll +0 -0
- package/libs/bin/windows/ia32/odin.lib +0 -0
- package/libs/bin/windows/ia32/odin_crypto.dll +0 -0
- package/libs/bin/windows/ia32/odin_crypto.lib +0 -0
- package/libs/bin/windows/x64/odin.dll +0 -0
- package/libs/bin/windows/x64/odin.lib +0 -0
- package/libs/bin/windows/x64/odin_crypto.dll +0 -0
- package/libs/bin/windows/x64/odin_crypto.lib +0 -0
- package/libs/include/odin.h +665 -567
- package/libs/include/odin_crypto.h +46 -0
- package/odin.cipher.d.ts +31 -0
- package/odin.media.d.ts +69 -19
- package/odin.room.d.ts +348 -7
- package/package.json +5 -4
- package/prebuilds/{darwin-arm64/node.napi.node → darwin-x64+arm64/libodin.dylib} +0 -0
- package/prebuilds/darwin-x64+arm64/libodin_crypto.dylib +0 -0
- package/prebuilds/darwin-x64+arm64/node.napi.node +0 -0
- package/prebuilds/linux-x64/libodin.so +0 -0
- package/prebuilds/linux-x64/libodin_crypto.so +0 -0
- package/prebuilds/linux-x64/node.napi.node +0 -0
- package/prebuilds/win32-x64/node.napi.node +0 -0
- package/prebuilds/win32-x64/odin.dll +0 -0
- package/prebuilds/win32-x64/odin_crypto.dll +0 -0
- package/scripts/postbuild.cjs +133 -0
- package/tests/audio-recording/README.md +97 -12
- package/tests/audio-recording/index.js +238 -130
- package/tests/connection-test/README.md +97 -0
- package/tests/connection-test/index.js +273 -0
- package/tests/lifecycle/test-room-cycle.js +169 -0
- package/tests/sending-audio/README.md +178 -9
- package/tests/sending-audio/canBounce.mp3 +0 -0
- package/tests/sending-audio/index.js +250 -87
- package/tests/sending-audio/test-kiss-api.js +149 -0
- package/tests/sending-audio/test-loop-audio.js +142 -0
- package/CMakeLists.txt +0 -25
- package/libs/bin/linux/arm64/libodin_static.a +0 -0
- package/libs/bin/linux/ia32/libodin_static.a +0 -0
- package/libs/bin/linux/x64/libodin_static.a +0 -0
- package/libs/bin/macos/arm64/libodin_static.a +0 -0
- package/libs/bin/macos/x64/libodin_static.a +0 -0
- package/libs/bin/windows/arm64/odin_static.lib +0 -0
- package/libs/bin/windows/ia32/odin_static.lib +0 -0
- package/libs/bin/windows/x64/odin_static.lib +0 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# ODIN Connection Test Example
|
|
2
|
+
|
|
3
|
+
This example demonstrates how to connect to an ODIN room and use the SDK's event system and diagnostic methods.
|
|
4
|
+
|
|
5
|
+
## Features Demonstrated
|
|
6
|
+
|
|
7
|
+
- **Room Connection**: Connecting to an ODIN room using the factory pattern
|
|
8
|
+
- **Event Handling**: All major event types (Joined, PeerJoined, MediaStarted, etc.)
|
|
9
|
+
- **End-to-End Encryption**: Optional E2EE configuration with OdinCipher
|
|
10
|
+
- **Diagnostic Methods**: Connection stats, jitter stats, and peer encryption status
|
|
11
|
+
|
|
12
|
+
## Prerequisites
|
|
13
|
+
|
|
14
|
+
You need an ODIN access key. Get one for free at [4Players ODIN](https://docs.4players.io/voice/introduction/access-keys/).
|
|
15
|
+
|
|
16
|
+
## Configuration
|
|
17
|
+
|
|
18
|
+
Edit `index.js` and replace the placeholder values:
|
|
19
|
+
|
|
20
|
+
```javascript
|
|
21
|
+
const accessKey = "__YOUR_ACCESS_KEY__"; // Your ODIN access key
|
|
22
|
+
const roomId = "__YOUR_ROOM_ID__"; // Room to join
|
|
23
|
+
const userId = "TestUser-123"; // Your user ID
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
To enable E2EE, set a password:
|
|
27
|
+
|
|
28
|
+
```javascript
|
|
29
|
+
const cipherPassword = "my-secret-password"; // All peers must use the same password
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Running the Example
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
cd tests/connection-test
|
|
36
|
+
node index.js
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## What This Example Does
|
|
40
|
+
|
|
41
|
+
1. **Initializes** the ODIN client
|
|
42
|
+
2. **Generates** a room token from your access key
|
|
43
|
+
3. **Configures** E2EE (if password is set)
|
|
44
|
+
4. **Sets up** event handlers for all room events
|
|
45
|
+
5. **Joins** the room and waits for 15 seconds
|
|
46
|
+
6. **Runs** diagnostic tests:
|
|
47
|
+
- `getConnectionId()` - Connection identifier
|
|
48
|
+
- `getConnectionStats()` - RTT, packet loss, bandwidth
|
|
49
|
+
- `getJitterStats()` - Audio jitter buffer metrics
|
|
50
|
+
- `getPeerStatus()` - E2EE peer verification
|
|
51
|
+
7. **Closes** the room and exits
|
|
52
|
+
|
|
53
|
+
## Expected Output
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
=== ODIN NodeJS SDK Connection Test ===
|
|
57
|
+
|
|
58
|
+
1. Initializing Odin Client...
|
|
59
|
+
2. Generating token from access key...
|
|
60
|
+
Token generated successfully.
|
|
61
|
+
|
|
62
|
+
3. Setting up Cipher (E2EE)...
|
|
63
|
+
E2EE disabled (no password configured).
|
|
64
|
+
|
|
65
|
+
4. Creating Room via client.createRoom()...
|
|
66
|
+
5. Setting up event handlers...
|
|
67
|
+
|
|
68
|
+
6. Joining room...
|
|
69
|
+
Join initiated.
|
|
70
|
+
|
|
71
|
+
[Event] ConnectionStateChanged: Connecting
|
|
72
|
+
[Event] ConnectionStateChanged: Joined
|
|
73
|
+
[Event] Joined room!
|
|
74
|
+
Room ID: my-room
|
|
75
|
+
Own Peer ID: 12345
|
|
76
|
+
Available Media IDs: [1, 2, 3, 4]
|
|
77
|
+
|
|
78
|
+
8. Testing diagnostic methods...
|
|
79
|
+
|
|
80
|
+
8a. getConnectionId():
|
|
81
|
+
Connection ID: 567890
|
|
82
|
+
✅ Connection ID retrieved successfully
|
|
83
|
+
|
|
84
|
+
8b. getConnectionStats():
|
|
85
|
+
✅ Connection stats retrieved:
|
|
86
|
+
- RTT: 25.50 ms
|
|
87
|
+
- TX Bytes: 1234
|
|
88
|
+
- RX Bytes: 5678
|
|
89
|
+
...
|
|
90
|
+
|
|
91
|
+
=== Test Complete ===
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Related Examples
|
|
95
|
+
|
|
96
|
+
- [audio-recording](../audio-recording/) - Record audio from peers
|
|
97
|
+
- [sending-audio](../sending-audio/) - Send audio to the room
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ODIN Node.js SDK - Connection Test Example
|
|
3
|
+
*
|
|
4
|
+
* This example demonstrates how to:
|
|
5
|
+
* - Connect to an ODIN room using the SDK
|
|
6
|
+
* - Handle various room and peer events
|
|
7
|
+
* - Use the new diagnostic methods (getConnectionStats, getJitterStats, etc.)
|
|
8
|
+
* - Configure End-to-End Encryption (E2EE) with OdinCipher
|
|
9
|
+
*
|
|
10
|
+
* Prerequisites:
|
|
11
|
+
* - Replace __YOUR_ACCESS_KEY__ with your ODIN access key
|
|
12
|
+
* - Get a free access key at https://www.4players.io/odin
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import odin from '../../index.cjs';
|
|
16
|
+
const { OdinClient, OdinCipher } = odin;
|
|
17
|
+
|
|
18
|
+
// ============================================
|
|
19
|
+
// Configuration - Replace with your values
|
|
20
|
+
// ============================================
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Your ODIN access key. Get one for free at https://www.4players.io/odin
|
|
24
|
+
* @type {string}
|
|
25
|
+
*/
|
|
26
|
+
const accessKey = "__YOUR_ACCESS_KEY__";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The room ID to join. All users joining the same room can communicate.
|
|
30
|
+
* @type {string}
|
|
31
|
+
*/
|
|
32
|
+
const roomId = "__YOUR_ROOM_ID__";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Your unique user identifier. Used to identify this connection.
|
|
36
|
+
* @type {string}
|
|
37
|
+
*/
|
|
38
|
+
const userId = "TestUser-" + Math.floor(Math.random() * 10000);
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Optional: Password for End-to-End Encryption.
|
|
42
|
+
* Set to null to disable E2EE, or set a string that all peers must share.
|
|
43
|
+
* @type {string|null}
|
|
44
|
+
*/
|
|
45
|
+
const cipherPassword = null;
|
|
46
|
+
|
|
47
|
+
// ============================================
|
|
48
|
+
// Main Test Function
|
|
49
|
+
// ============================================
|
|
50
|
+
|
|
51
|
+
async function run() {
|
|
52
|
+
console.log("=== ODIN NodeJS SDK Connection Test ===\n");
|
|
53
|
+
|
|
54
|
+
// Step 1: Create the ODIN client
|
|
55
|
+
console.log("1. Initializing Odin Client...");
|
|
56
|
+
const client = new OdinClient();
|
|
57
|
+
|
|
58
|
+
// Step 2: Generate token locally using access key
|
|
59
|
+
// This replaces the old REST API token creation method
|
|
60
|
+
console.log("2. Generating token from access key...");
|
|
61
|
+
const token = client.generateToken(accessKey, roomId, userId);
|
|
62
|
+
console.log(" Token generated successfully.\n");
|
|
63
|
+
|
|
64
|
+
// Step 3: Set up E2EE cipher (optional)
|
|
65
|
+
console.log("3. Setting up Cipher (E2EE)...");
|
|
66
|
+
let cipher = null;
|
|
67
|
+
try {
|
|
68
|
+
if (cipherPassword && typeof OdinCipher === 'function') {
|
|
69
|
+
cipher = new OdinCipher();
|
|
70
|
+
const key = new TextEncoder().encode(cipherPassword);
|
|
71
|
+
cipher.setPassword(key);
|
|
72
|
+
console.log(" Cipher created and password set.\n");
|
|
73
|
+
} else {
|
|
74
|
+
console.log(" E2EE disabled (no password configured).\n");
|
|
75
|
+
}
|
|
76
|
+
} catch (e) {
|
|
77
|
+
console.log(" OdinCipher creation failed:", e.message, "\n");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Step 4: Create room using factory pattern (recommended)
|
|
81
|
+
console.log("4. Creating Room via client.createRoom()...");
|
|
82
|
+
const room = client.createRoom(token);
|
|
83
|
+
if (cipher) {
|
|
84
|
+
room.setCipher(cipher);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Step 5: Set up event handlers
|
|
88
|
+
console.log("5. Setting up event handlers...\n");
|
|
89
|
+
|
|
90
|
+
// Connection state handler - tracks connection lifecycle
|
|
91
|
+
room.onConnectionStateChanged((event) => {
|
|
92
|
+
console.log(`[Event] ConnectionStateChanged: ${event.state}${event.message ? ` (${event.message})` : ''}`);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Joined handler - fires when successfully connected to the room
|
|
96
|
+
room.onJoined((event) => {
|
|
97
|
+
console.log("[Event] Joined room!");
|
|
98
|
+
console.log(` Room ID: ${event.roomId}`);
|
|
99
|
+
console.log(` Own Peer ID: ${event.ownPeerId}`);
|
|
100
|
+
console.log(` Available Media IDs: ${JSON.stringify(event.mediaIds)}`);
|
|
101
|
+
if (event.room?.peers?.length > 0) {
|
|
102
|
+
console.log(` Existing peers: ${event.room.peers.length}`);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Left handler - fires when disconnected from room
|
|
107
|
+
room.onLeft((event) => {
|
|
108
|
+
console.log(`[Event] Left room. Reason: ${event.reason}`);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Peer joined handler - fires when another user joins
|
|
112
|
+
room.onPeerJoined((event) => {
|
|
113
|
+
console.log(`[Event] Peer joined: ID=${event.peerId}, UserID=${event.userId}`);
|
|
114
|
+
if (event.userData) {
|
|
115
|
+
try {
|
|
116
|
+
const userData = JSON.parse(new TextDecoder().decode(new Uint8Array(event.userData)));
|
|
117
|
+
console.log(` User data:`, userData);
|
|
118
|
+
} catch (e) {
|
|
119
|
+
console.log(` User data: (binary, ${event.userData?.length || 0} bytes)`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Peer left handler - fires when another user leaves
|
|
125
|
+
room.onPeerLeft((event) => {
|
|
126
|
+
console.log(`[Event] Peer left: ID=${event.peerId}`);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Media started handler - fires when a peer starts streaming audio
|
|
130
|
+
room.onMediaStarted((event) => {
|
|
131
|
+
console.log(`[Event] Media started: Peer=${event.peerId}, MediaID=${event.media?.id}`);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Media stopped handler - fires when a peer stops streaming
|
|
135
|
+
room.onMediaStopped((event) => {
|
|
136
|
+
console.log(`[Event] Media stopped: Peer=${event.peerId}, MediaID=${event.mediaId}`);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Message received handler - fires when a peer sends a message
|
|
140
|
+
room.onMessageReceived((event) => {
|
|
141
|
+
console.log(`[Event] Message received from Peer=${event.senderPeerId}`);
|
|
142
|
+
try {
|
|
143
|
+
const message = JSON.parse(new TextDecoder().decode(new Uint8Array(event.message)));
|
|
144
|
+
console.log(` Message:`, message);
|
|
145
|
+
} catch (e) {
|
|
146
|
+
console.log(` Message: (binary, ${event.message?.length || 0} bytes)`);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Audio data handler - fires for each audio frame received
|
|
151
|
+
// Note: This fires frequently, so we only log occasionally to avoid spam
|
|
152
|
+
room.onAudioDataReceived((data) => {
|
|
153
|
+
if (Math.random() < 0.01) {
|
|
154
|
+
console.log(`[Event] Audio data: MediaID=${data.mediaId}, samples=${data.samples16?.length || 0}`);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Step 6: Join the room
|
|
159
|
+
console.log("6. Joining room...");
|
|
160
|
+
try {
|
|
161
|
+
room.join("https://gateway.odin.4players.io");
|
|
162
|
+
console.log(" Join initiated.\n");
|
|
163
|
+
} catch (e) {
|
|
164
|
+
console.error(" Join failed:", e.message);
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Step 7: Wait for connection to establish and events to arrive
|
|
169
|
+
console.log("7. Waiting 15 seconds for events...\n");
|
|
170
|
+
await new Promise(resolve => setTimeout(resolve, 15000));
|
|
171
|
+
|
|
172
|
+
// =========================================
|
|
173
|
+
// Step 8: Test Diagnostic Methods
|
|
174
|
+
// =========================================
|
|
175
|
+
console.log("\n8. Testing diagnostic methods...\n");
|
|
176
|
+
|
|
177
|
+
// Test getConnectionId() - retrieves the underlying connection identifier
|
|
178
|
+
console.log(" 8a. getConnectionId():");
|
|
179
|
+
try {
|
|
180
|
+
const connectionId = room.getConnectionId();
|
|
181
|
+
console.log(` Connection ID: ${connectionId}`);
|
|
182
|
+
if (connectionId === 0) {
|
|
183
|
+
console.log(" ⚠️ Connection ID is 0 (might indicate disconnected state)");
|
|
184
|
+
} else {
|
|
185
|
+
console.log(" ✅ Connection ID retrieved successfully");
|
|
186
|
+
}
|
|
187
|
+
} catch (e) {
|
|
188
|
+
console.log(` ❌ Error: ${e.message}`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Test getConnectionStats() - retrieves network quality metrics
|
|
192
|
+
console.log("\n 8b. getConnectionStats():");
|
|
193
|
+
try {
|
|
194
|
+
const stats = room.getConnectionStats();
|
|
195
|
+
if (stats) {
|
|
196
|
+
console.log(" ✅ Connection stats retrieved:");
|
|
197
|
+
console.log(` - RTT: ${stats.rtt.toFixed(2)} ms`);
|
|
198
|
+
console.log(` - TX Datagrams: ${stats.udpTxDatagrams}`);
|
|
199
|
+
console.log(` - TX Bytes: ${stats.udpTxBytes}`);
|
|
200
|
+
console.log(` - TX Loss: ${(stats.udpTxLoss * 100).toFixed(2)}%`);
|
|
201
|
+
console.log(` - RX Datagrams: ${stats.udpRxDatagrams}`);
|
|
202
|
+
console.log(` - RX Bytes: ${stats.udpRxBytes}`);
|
|
203
|
+
console.log(` - RX Loss: ${(stats.udpRxLoss * 100).toFixed(2)}%`);
|
|
204
|
+
console.log(` - Cwnd: ${stats.cwnd}`);
|
|
205
|
+
console.log(` - Congestion Events: ${stats.congestionEvents}`);
|
|
206
|
+
} else {
|
|
207
|
+
console.log(" ⚠️ Stats returned null (room might be disconnected)");
|
|
208
|
+
}
|
|
209
|
+
} catch (e) {
|
|
210
|
+
console.log(` ❌ Error: ${e.message}`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Test getJitterStats() - retrieves audio jitter buffer statistics
|
|
214
|
+
console.log("\n 8c. getJitterStats(mediaId):");
|
|
215
|
+
try {
|
|
216
|
+
// Try to get jitter stats for media ID 1 (may not exist if no audio received)
|
|
217
|
+
const jitterStats = room.getJitterStats(1);
|
|
218
|
+
if (jitterStats) {
|
|
219
|
+
console.log(" ✅ Jitter stats retrieved for media ID 1:");
|
|
220
|
+
console.log(` - Packets Total: ${jitterStats.packetsTotal}`);
|
|
221
|
+
console.log(` - Packets Buffered: ${jitterStats.packetsBuffered}`);
|
|
222
|
+
console.log(` - Packets Processed: ${jitterStats.packetsProcessed}`);
|
|
223
|
+
console.log(` - Packets Too Early: ${jitterStats.packetsArrivedTooEarly}`);
|
|
224
|
+
console.log(` - Packets Too Late: ${jitterStats.packetsArrivedTooLate}`);
|
|
225
|
+
console.log(` - Packets Dropped: ${jitterStats.packetsDropped}`);
|
|
226
|
+
console.log(` - Packets Invalid: ${jitterStats.packetsInvalid}`);
|
|
227
|
+
console.log(` - Packets Repeated: ${jitterStats.packetsRepeated}`);
|
|
228
|
+
console.log(` - Packets Lost: ${jitterStats.packetsLost}`);
|
|
229
|
+
} else {
|
|
230
|
+
console.log(" ⚠️ Jitter stats returned null (no decoder for media ID 1 - this is expected if no audio received)");
|
|
231
|
+
}
|
|
232
|
+
} catch (e) {
|
|
233
|
+
console.log(` ❌ Error: ${e.message}`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Test cipher.getPeerStatus() - verifies E2EE status for peers
|
|
237
|
+
console.log("\n 8d. cipher.getPeerStatus(peerId):");
|
|
238
|
+
if (cipher) {
|
|
239
|
+
try {
|
|
240
|
+
// Test with own peer ID
|
|
241
|
+
const ownPeerId = room.ownPeerId;
|
|
242
|
+
if (ownPeerId) {
|
|
243
|
+
const status = cipher.getPeerStatus(ownPeerId);
|
|
244
|
+
console.log(` ✅ Peer status for own ID (${ownPeerId}): "${status}"`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Test with a non-existent peer ID
|
|
248
|
+
const unknownStatus = cipher.getPeerStatus(99999);
|
|
249
|
+
console.log(` ✅ Peer status for unknown peer (99999): "${unknownStatus}"`);
|
|
250
|
+
} catch (e) {
|
|
251
|
+
console.log(` ❌ Error: ${e.message}`);
|
|
252
|
+
}
|
|
253
|
+
} else {
|
|
254
|
+
console.log(" ⚠️ Cipher not available, skipping getPeerStatus test");
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Step 9: Clean up
|
|
258
|
+
console.log("\n9. Closing room...");
|
|
259
|
+
room.close();
|
|
260
|
+
console.log(" Room closed.\n");
|
|
261
|
+
|
|
262
|
+
console.log("=== Test Complete ===");
|
|
263
|
+
process.exit(0);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ============================================
|
|
267
|
+
// Entry Point
|
|
268
|
+
// ============================================
|
|
269
|
+
|
|
270
|
+
run().catch(err => {
|
|
271
|
+
console.error("Error:", err);
|
|
272
|
+
process.exit(1);
|
|
273
|
+
});
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ODIN Node.js SDK - Room Lifecycle Stress Test
|
|
3
|
+
*
|
|
4
|
+
* This test mimics the user's transcription bot architecture:
|
|
5
|
+
* - Single OdinClient (singleton, lives for entire process)
|
|
6
|
+
* - Multiple rooms created/destroyed over time
|
|
7
|
+
* - MP3 playback on each join
|
|
8
|
+
* - Tests for memory leaks and native crashes
|
|
9
|
+
*
|
|
10
|
+
* If this test completes without crashes, the SDK handles the lifecycle correctly.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import odin from '../../index.cjs';
|
|
14
|
+
const { OdinClient, OdinCipher } = odin;
|
|
15
|
+
import path from 'path';
|
|
16
|
+
import { fileURLToPath } from 'url';
|
|
17
|
+
|
|
18
|
+
// Get the directory of the current script
|
|
19
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
20
|
+
const __dirname = path.dirname(__filename);
|
|
21
|
+
|
|
22
|
+
// ============================================
|
|
23
|
+
// Configuration
|
|
24
|
+
// ============================================
|
|
25
|
+
|
|
26
|
+
const accessKey = "__YOUR_ACCESS_KEY__/kTC2Yhitf4fJSx95jpN3F9Xac3";
|
|
27
|
+
const roomId = "__YOUR_ROOM_ID__";
|
|
28
|
+
const cipherPassword = "__YOUR_CIPHER_PASSWORD_OR_NULL__";
|
|
29
|
+
|
|
30
|
+
// Number of room create/destroy cycles
|
|
31
|
+
const CYCLE_COUNT = 5;
|
|
32
|
+
|
|
33
|
+
// Delay between cycles (milliseconds)
|
|
34
|
+
const DELAY_BETWEEN_CYCLES = 2000;
|
|
35
|
+
|
|
36
|
+
// Audio file to play (optional - skip if not found)
|
|
37
|
+
const audioFile = path.join(__dirname, '../sending-audio/canBounce.mp3');
|
|
38
|
+
|
|
39
|
+
// ============================================
|
|
40
|
+
// Main Test Function
|
|
41
|
+
// ============================================
|
|
42
|
+
|
|
43
|
+
async function main() {
|
|
44
|
+
console.log("=== ODIN Node.js SDK - Room Lifecycle Stress Test ===\n");
|
|
45
|
+
console.log(`Testing ${CYCLE_COUNT} room create/destroy cycles with ${DELAY_BETWEEN_CYCLES}ms delay.\n`);
|
|
46
|
+
|
|
47
|
+
// Step 1: Create ODIN client (singleton - lives for entire test)
|
|
48
|
+
console.log("1. Creating OdinClient (singleton)...");
|
|
49
|
+
const client = new OdinClient();
|
|
50
|
+
console.log(" OdinClient created.\n");
|
|
51
|
+
|
|
52
|
+
// Run multiple cycles
|
|
53
|
+
for (let cycle = 1; cycle <= CYCLE_COUNT; cycle++) {
|
|
54
|
+
console.log(`\n========== CYCLE ${cycle}/${CYCLE_COUNT} ==========\n`);
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
await runCycle(client, cycle);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error(` ❌ CYCLE ${cycle} FAILED: ${error.message}`);
|
|
60
|
+
console.error(error.stack);
|
|
61
|
+
// Continue to next cycle to see if it's recoverable
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Wait between cycles
|
|
65
|
+
if (cycle < CYCLE_COUNT) {
|
|
66
|
+
console.log(`\n ⏳ Waiting ${DELAY_BETWEEN_CYCLES}ms before next cycle...`);
|
|
67
|
+
await new Promise(r => setTimeout(r, DELAY_BETWEEN_CYCLES));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
console.log("\n========== TEST COMPLETE ==========\n");
|
|
72
|
+
console.log("✅ All cycles completed without native crash!");
|
|
73
|
+
console.log(" If you see this message, the lifecycle fixes are working.\n");
|
|
74
|
+
|
|
75
|
+
process.exit(0);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Run a single room create/join/play/close cycle
|
|
80
|
+
*/
|
|
81
|
+
async function runCycle(client, cycleNum) {
|
|
82
|
+
const userId = `LifecycleBot-Cycle${cycleNum}-${Math.floor(Math.random() * 10000)}`;
|
|
83
|
+
|
|
84
|
+
// Step A: Generate token
|
|
85
|
+
console.log(`${cycleNum}.A Generating token...`);
|
|
86
|
+
const token = client.generateToken(accessKey, roomId, userId);
|
|
87
|
+
|
|
88
|
+
// Step B: Create room
|
|
89
|
+
console.log(`${cycleNum}.B Creating room...`);
|
|
90
|
+
const room = client.createRoom(token);
|
|
91
|
+
|
|
92
|
+
// Step C: Set up cipher (E2EE)
|
|
93
|
+
console.log(`${cycleNum}.C Setting up cipher...`);
|
|
94
|
+
const cipher = new OdinCipher();
|
|
95
|
+
cipher.setPassword(new TextEncoder().encode(cipherPassword));
|
|
96
|
+
room.setCipher(cipher);
|
|
97
|
+
|
|
98
|
+
// Step D: Set up event handlers
|
|
99
|
+
const joinPromise = new Promise((resolve, reject) => {
|
|
100
|
+
const timeout = setTimeout(() => reject(new Error('Join timeout')), 10000);
|
|
101
|
+
room.onJoined((event) => {
|
|
102
|
+
clearTimeout(timeout);
|
|
103
|
+
resolve(event);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Track if we received any audio
|
|
108
|
+
let audioPacketsReceived = 0;
|
|
109
|
+
room.onAudioDataReceived(() => {
|
|
110
|
+
audioPacketsReceived++;
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Step E: Join room
|
|
114
|
+
console.log(`${cycleNum}.D Joining room...`);
|
|
115
|
+
const userData = new TextEncoder().encode(JSON.stringify({
|
|
116
|
+
name: `Lifecycle Test Bot (Cycle ${cycleNum})`,
|
|
117
|
+
platform: "ODIN Node.js SDK"
|
|
118
|
+
}));
|
|
119
|
+
room.join("https://gateway.odin.4players.io", userData);
|
|
120
|
+
|
|
121
|
+
// Wait for join
|
|
122
|
+
const joinEvent = await joinPromise;
|
|
123
|
+
console.log(` ✓ Joined! Peer ID: ${joinEvent.ownPeerId}, Media IDs: ${room.availableMediaIds.length}`);
|
|
124
|
+
|
|
125
|
+
// Step F: Play MP3 (if file exists)
|
|
126
|
+
let audioPlayed = false;
|
|
127
|
+
try {
|
|
128
|
+
const fs = await import('fs');
|
|
129
|
+
if (fs.existsSync(audioFile)) {
|
|
130
|
+
console.log(`${cycleNum}.E Creating audio stream and playing MP3...`);
|
|
131
|
+
const media = room.createAudioStream(44100, 2);
|
|
132
|
+
await media.sendMP3(audioFile);
|
|
133
|
+
console.log(` ✓ MP3 playback complete`);
|
|
134
|
+
|
|
135
|
+
// Small delay before closing
|
|
136
|
+
await new Promise(r => setTimeout(r, 200));
|
|
137
|
+
|
|
138
|
+
console.log(`${cycleNum}.F Closing audio stream...`);
|
|
139
|
+
media.close();
|
|
140
|
+
console.log(` ✓ Audio stream closed. Available IDs: ${room.availableMediaIds.length}`);
|
|
141
|
+
audioPlayed = true;
|
|
142
|
+
} else {
|
|
143
|
+
console.log(`${cycleNum}.E Skipping MP3 playback (file not found)`);
|
|
144
|
+
}
|
|
145
|
+
} catch (err) {
|
|
146
|
+
console.log(` ⚠ MP3 playback error: ${err.message}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Step G: Wait a moment to simulate session activity
|
|
150
|
+
console.log(`${cycleNum}.G Simulating session activity (500ms)...`);
|
|
151
|
+
await new Promise(r => setTimeout(r, 500));
|
|
152
|
+
console.log(` ✓ Received ${audioPacketsReceived} audio packets`);
|
|
153
|
+
|
|
154
|
+
// Step H: Close room
|
|
155
|
+
console.log(`${cycleNum}.H Closing room...`);
|
|
156
|
+
room.close();
|
|
157
|
+
console.log(` ✓ Room closed`);
|
|
158
|
+
|
|
159
|
+
console.log(`\n ✅ Cycle ${cycleNum} completed successfully!`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ============================================
|
|
163
|
+
// Entry Point
|
|
164
|
+
// ============================================
|
|
165
|
+
|
|
166
|
+
main().catch(err => {
|
|
167
|
+
console.error("\n❌ TEST FAILED WITH ERROR:", err);
|
|
168
|
+
process.exit(1);
|
|
169
|
+
});
|