@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.
Files changed (73) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/LICENSE +21 -0
  3. package/README.md +603 -44
  4. package/binding.gyp +29 -13
  5. package/cppsrc/binding.cpp +3 -6
  6. package/cppsrc/odinbindings.cpp +9 -45
  7. package/cppsrc/odincipher.cpp +92 -0
  8. package/cppsrc/odincipher.h +32 -0
  9. package/cppsrc/odinclient.cpp +19 -158
  10. package/cppsrc/odinclient.h +2 -5
  11. package/cppsrc/odinmedia.cpp +144 -186
  12. package/cppsrc/odinmedia.h +51 -18
  13. package/cppsrc/odinroom.cpp +675 -635
  14. package/cppsrc/odinroom.h +76 -26
  15. package/cppsrc/utilities.cpp +11 -81
  16. package/cppsrc/utilities.h +25 -140
  17. package/index.cjs +829 -0
  18. package/index.d.ts +3 -4
  19. package/libs/bin/linux/arm64/libodin.so +0 -0
  20. package/libs/bin/linux/arm64/libodin_crypto.so +0 -0
  21. package/libs/bin/linux/ia32/libodin.so +0 -0
  22. package/libs/bin/linux/ia32/libodin_crypto.so +0 -0
  23. package/libs/bin/linux/x64/libodin.so +0 -0
  24. package/libs/bin/linux/x64/libodin_crypto.so +0 -0
  25. package/{prebuilds/darwin-x64/node.napi.node → libs/bin/macos/universal/libodin.dylib} +0 -0
  26. package/libs/bin/macos/universal/libodin_crypto.dylib +0 -0
  27. package/libs/bin/windows/arm64/odin.dll +0 -0
  28. package/libs/bin/windows/arm64/odin.lib +0 -0
  29. package/libs/bin/windows/arm64/odin_crypto.dll +0 -0
  30. package/libs/bin/windows/arm64/odin_crypto.lib +0 -0
  31. package/libs/bin/windows/ia32/odin.dll +0 -0
  32. package/libs/bin/windows/ia32/odin.lib +0 -0
  33. package/libs/bin/windows/ia32/odin_crypto.dll +0 -0
  34. package/libs/bin/windows/ia32/odin_crypto.lib +0 -0
  35. package/libs/bin/windows/x64/odin.dll +0 -0
  36. package/libs/bin/windows/x64/odin.lib +0 -0
  37. package/libs/bin/windows/x64/odin_crypto.dll +0 -0
  38. package/libs/bin/windows/x64/odin_crypto.lib +0 -0
  39. package/libs/include/odin.h +665 -567
  40. package/libs/include/odin_crypto.h +46 -0
  41. package/odin.cipher.d.ts +31 -0
  42. package/odin.media.d.ts +69 -19
  43. package/odin.room.d.ts +348 -7
  44. package/package.json +5 -4
  45. package/prebuilds/{darwin-arm64/node.napi.node → darwin-x64+arm64/libodin.dylib} +0 -0
  46. package/prebuilds/darwin-x64+arm64/libodin_crypto.dylib +0 -0
  47. package/prebuilds/darwin-x64+arm64/node.napi.node +0 -0
  48. package/prebuilds/linux-x64/libodin.so +0 -0
  49. package/prebuilds/linux-x64/libodin_crypto.so +0 -0
  50. package/prebuilds/linux-x64/node.napi.node +0 -0
  51. package/prebuilds/win32-x64/node.napi.node +0 -0
  52. package/prebuilds/win32-x64/odin.dll +0 -0
  53. package/prebuilds/win32-x64/odin_crypto.dll +0 -0
  54. package/scripts/postbuild.cjs +133 -0
  55. package/tests/audio-recording/README.md +97 -12
  56. package/tests/audio-recording/index.js +238 -130
  57. package/tests/connection-test/README.md +97 -0
  58. package/tests/connection-test/index.js +273 -0
  59. package/tests/lifecycle/test-room-cycle.js +169 -0
  60. package/tests/sending-audio/README.md +178 -9
  61. package/tests/sending-audio/canBounce.mp3 +0 -0
  62. package/tests/sending-audio/index.js +250 -87
  63. package/tests/sending-audio/test-kiss-api.js +149 -0
  64. package/tests/sending-audio/test-loop-audio.js +142 -0
  65. package/CMakeLists.txt +0 -25
  66. package/libs/bin/linux/arm64/libodin_static.a +0 -0
  67. package/libs/bin/linux/ia32/libodin_static.a +0 -0
  68. package/libs/bin/linux/x64/libodin_static.a +0 -0
  69. package/libs/bin/macos/arm64/libodin_static.a +0 -0
  70. package/libs/bin/macos/x64/libodin_static.a +0 -0
  71. package/libs/bin/windows/arm64/odin_static.lib +0 -0
  72. package/libs/bin/windows/ia32/odin_static.lib +0 -0
  73. 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
+ });