@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
@@ -1,20 +1,189 @@
1
- # ODIN Send Music Example
1
+ # ODIN Audio Sending Examples
2
2
 
3
- This sample demonstrates how to use the ODIN SDK to send music to other users in a room. It loads a sample MP3 file,
4
- decodes it and sends it to the ODIN server.
3
+ This folder contains examples demonstrating how to send audio to an ODIN room using both the **high-level** and **low-level** APIs.
4
+
5
+ ## Examples
6
+
7
+ | File | API Level | Description |
8
+ |------|-----------|-------------|
9
+ | `test-kiss-api.js` | **High-Level** | Simplified API - just call `media.sendMP3()` |
10
+ | `index.js` | **Low-Level** | Full control over audio transmission |
5
11
 
6
12
  ## Prerequisites
7
13
 
8
- You need to have an ODIN Access Key. See [here](https://www.4players.io/odin/introduction/access-keys/#generating-access-keys))
9
- for more info on the topic and to create one for free directly in the documentation.
14
+ 1. **ODIN Access Key**: Get one for free at [4Players ODIN](https://www.4players.io/odin/introduction/access-keys/)
15
+ 2. **Audio File**: A sample `santa.mp3` is included for testing
16
+
17
+ ## Configuration
18
+
19
+ Edit either script and replace the placeholder values:
20
+
21
+ ```javascript
22
+ const accessKey = "__YOUR_ACCESS_KEY__"; // Your ODIN access key
23
+ const roomId = "__YOUR_ROOM_ID__"; // Room to send audio to
24
+ const userId = "AudioBot-123"; // Bot's user ID
25
+ ```
26
+
27
+ To enable E2EE (must match other peers):
28
+
29
+ ```javascript
30
+ const cipherPassword = "shared-secret";
31
+ ```
32
+
33
+ ---
34
+
35
+ ## High-Level API (`test-kiss-api.js`)
36
+
37
+ The high-level API is the **recommended approach** for most use cases. It handles all the complexity automatically.
10
38
 
11
- ## Getting Started
39
+ ### Features
40
+ - **Automatic Setup**: Claims media ID, sends StartMedia RPC
41
+ - **Audio Decoding**: Supports MP3, WAV, and other formats
42
+ - **Precise Timing**: Streams audio in 20ms chunks with correct timing
43
+ - **Simple Interface**: One method call to send an entire file
12
44
 
13
- Run the script with the following command:
45
+ ### Running
14
46
 
15
47
  ```bash
48
+ cd tests/sending-audio
49
+ node test-kiss-api.js
50
+ ```
51
+
52
+ ### Code Example
53
+
54
+ ```javascript
55
+ // Create room and wait for join
56
+ const room = client.createRoom(token);
57
+ await new Promise(resolve => room.onJoined(resolve));
58
+ room.join("https://gateway.odin.4players.io");
59
+
60
+ // Create audio stream
61
+ const media = room.createAudioStream(44100, 2);
62
+
63
+ // Send audio with one line!
64
+ await media.sendMP3('./music.mp3');
65
+
66
+ // Or send other formats
67
+ await media.sendWAV('./audio.wav');
68
+ await media.sendBuffer(audioBuffer); // Decoded AudioBuffer
69
+
70
+ // Clean up
71
+ media.close();
72
+ ```
73
+
74
+ ---
75
+
76
+ ## Low-Level API (`index.js`)
77
+
78
+ The low-level API provides **full control** over audio transmission. Use it when you need:
79
+
80
+ - Custom audio sources (microphone, synthesized audio, real-time processing)
81
+ - Dynamic sample rate or channel configuration
82
+ - Fine-grained timing control
83
+ - Integration with custom audio pipelines
84
+
85
+ ### Running
86
+
87
+ ```bash
88
+ cd tests/sending-audio
16
89
  node index.js
17
90
  ```
18
91
 
19
- It will connect to the room `Lobby` using your access key and will send a "Hello World" message to the chat. It will then
20
- start sending an MP3 file into the room.
92
+ ### Workflow
93
+
94
+ 1. **Join Room** and wait for `Joined` event
95
+ 2. **Get Media ID** from `event.mediaIds`
96
+ 3. **Create Audio Stream** with sample rate and channels
97
+ 4. **Set Media ID** on the stream
98
+ 5. **Send StartMedia RPC** to notify server
99
+ 6. **Send Audio Data** in 20ms chunks
100
+ 7. **Close** when done
101
+
102
+ ### Code Example
103
+
104
+ ```javascript
105
+ import { encode } from '@msgpack/msgpack';
106
+
107
+ room.onJoined(async (event) => {
108
+ // Get media ID from the event
109
+ const mediaId = event.mediaIds[0];
110
+
111
+ // Create audio stream
112
+ const media = room.createAudioStream(48000, 2);
113
+
114
+ // Set the server-assigned media ID
115
+ media.setMediaId(mediaId);
116
+
117
+ // Notify server we're about to transmit
118
+ const rpc = encode([0, 1, "StartMedia", {
119
+ media_id: mediaId,
120
+ properties: { kind: "audio" }
121
+ }]);
122
+ room.sendRpc(new Uint8Array(rpc));
123
+
124
+ // Send audio in 20ms chunks
125
+ const chunkMs = 20;
126
+ const samplesPerChunk = Math.floor(48000 * chunkMs / 1000) * 2;
127
+
128
+ for (const chunk of audioChunks) {
129
+ media.sendAudioData(chunk);
130
+ await sleep(chunkMs);
131
+ }
132
+
133
+ // Clean up
134
+ media.close();
135
+ });
136
+ ```
137
+
138
+ ---
139
+
140
+ ## Audio Format Requirements
141
+
142
+ ### Input Audio
143
+ - **Formats**: MP3, WAV, OGG, FLAC (via audio-decode)
144
+ - **Sample Rate**: Any (encoder handles resampling)
145
+ - **Channels**: Mono or Stereo
146
+
147
+ ### sendAudioData() Requirements
148
+ - **Type**: `Float32Array`
149
+ - **Range**: `-1.0` to `1.0`
150
+ - **Format**: Interleaved if stereo (L, R, L, R, ...)
151
+ - **Chunk Size**: 20ms worth of samples
152
+
153
+ ---
154
+
155
+ ## Comparison
156
+
157
+ | Feature | High-Level API | Low-Level API |
158
+ |---------|---------------|---------------|
159
+ | Setup Complexity | Automatic | Manual |
160
+ | Media ID Handling | Automatic | Manual |
161
+ | StartMedia RPC | Automatic | Manual |
162
+ | Timing | Automatic | Manual |
163
+ | Custom Sources | ❌ | ✅ |
164
+ | Real-time Audio | ❌ | ✅ |
165
+ | Audio Processing | ❌ | ✅ |
166
+
167
+ ---
168
+
169
+ ## Troubleshooting
170
+
171
+ ### "No available media IDs" Error
172
+ Wait for the `Joined` event before sending audio. The SDK provides media IDs in this event.
173
+
174
+ ### Audio Not Heard
175
+ 1. Check E2EE password matches other peers
176
+ 2. Verify you're in the same room
177
+ 3. Check that other peers have their audio output enabled
178
+
179
+ ### Audio Choppy
180
+ - Ensure 20ms timing is maintained
181
+ - Check network connection quality
182
+ - Monitor jitter stats with `room.getJitterStats(mediaId)`
183
+
184
+ ---
185
+
186
+ ## Related Examples
187
+
188
+ - [connection-test](../connection-test/) - Basic connection and events
189
+ - [audio-recording](../audio-recording/) - Record audio from peers
@@ -1,107 +1,270 @@
1
- const accessKey = "ASBKPRwUpEabybkUJMfdLD2b2J8K2mus1ekxMUK7VPEA";
2
- const roomName = "Lobby";
3
- const userName = "My Bot";
1
+ /**
2
+ * ODIN Node.js SDK - Audio Sending Example (Low-Level API)
3
+ *
4
+ * This example demonstrates the LOW-LEVEL API for sending audio to an ODIN room.
5
+ * Use this approach when you need full control over audio transmission, such as:
6
+ * - Custom audio sources (microphone, synthesized audio)
7
+ * - Dynamic sample rate or channel configuration
8
+ * - Fine-grained timing control
9
+ * - Integration with custom audio processing pipelines
10
+ *
11
+ * For simpler use cases, see test-kiss-api.js which uses the HIGH-LEVEL API.
12
+ *
13
+ * Workflow:
14
+ * 1. Join the room and wait for the Joined event
15
+ * 2. Get media ID from the event's mediaIds array
16
+ * 3. Create audio stream with sample rate and channels
17
+ * 4. Set the media ID on the stream
18
+ * 5. Send StartMedia RPC to notify the server
19
+ * 6. Push audio samples in 20ms chunks
20
+ * 7. Close when done
21
+ *
22
+ * Prerequisites:
23
+ * - Replace __YOUR_ACCESS_KEY__ with your ODIN access key
24
+ * - Get a free access key at https://docs.4players.io/voice/introduction/access-keys/
25
+ */
4
26
 
5
- // Load the odin module and other libs
6
27
  import odin from '../../index.cjs';
7
- const {OdinClient} = odin;
28
+ const { OdinClient, OdinCipher } = odin;
8
29
  import fs from 'fs';
9
- import decode, {decoders} from 'audio-decode';
10
- import AudioBufferStream from 'audio-buffer-stream';
11
-
12
- // Create an odin client instance using our access key and create a room
13
- const userData = {
14
- name: "Music Bot",
15
- avatar: "https://avatars.dicebear.com/api/bottts/123.svg?backgroundColor=%23333333&textureChance=0&margin=10",
16
- seed: "123",
17
- userId: "Bot007",
18
- outputMuted: 1,
19
- inputMuted: 0,
20
- platform: "ODIN JS Bot SDK",
21
- version: "0.1"
22
- }
23
- const data = new TextEncoder().encode(JSON.stringify(userData));
24
- const odinClient = new OdinClient(48000, 2);
30
+ import decode from 'audio-decode';
31
+ import path from 'path';
32
+ import { fileURLToPath } from 'url';
33
+ import { encode } from '@msgpack/msgpack';
25
34
 
26
- // Create a token then join the room with that token, token may also be created elsewhere
27
- const token = odinClient.generateToken(accessKey, roomName, userName);
28
- const room = odinClient.createRoomWithToken(token);
35
+ // Get the directory of the current script (ESM equivalent of __dirname)
36
+ const __filename = fileURLToPath(import.meta.url);
37
+ const __dirname = path.dirname(__filename);
29
38
 
30
- // It's also possible to create a room with an access key, room name and username then token is created internally
31
- //const room = odinClient.createRoom(accessKey, roomName, userName);
39
+ // ============================================
40
+ // Configuration - Replace with your values
41
+ // ============================================
32
42
 
33
- // Join the room
34
- room.join("gateway.odin.4players.io", data);
43
+ /**
44
+ * Your ODIN access key. Get one for free at https://docs.4players.io/voice/introduction/access-keys/
45
+ * @type {string}
46
+ */
47
+ const accessKey = "__YOUR_ACCESS_KEY__";
35
48
 
36
- // Send a message to the room
37
- const message = {
38
- kind: 'message',
39
- payload: 'Hello, I am a music bot and will stream some music to you.'
40
- }
41
- room.sendMessage(new TextEncoder().encode(JSON.stringify(message)));
42
-
43
- // Send music to the room
44
- const sendMusic = async (media) => {
45
- // Prepare our MP3 decoder and load the sample file
46
- const audioBuffer = await decode(fs.readFileSync('./santa.mp3'));
47
-
48
- // Create a stream that will match the settings of the file
49
- const audioBufferStream = new AudioBufferStream({
50
- channels: audioBuffer.numberOfChannels,
51
- sampleRate: audioBuffer.sampleRate,
52
- float: true,
53
- bitDepth: 32,
54
- chunkLength: 960 // 960 bytes every 20ms - might be doubled (1920) depending on the sample rate
55
- });
49
+ /**
50
+ * The room ID to join. All users joining the same room can hear your audio.
51
+ * @type {string}
52
+ */
53
+ const roomId = "__YOUR_ROOM_ID__";
56
54
 
57
- // Create a queue to store the chunks of audio data
58
- const queue = [];
55
+ /**
56
+ * Your unique user identifier.
57
+ * @type {string}
58
+ */
59
+ const userId = "AudioBot-" + Math.floor(Math.random() * 10000);
59
60
 
60
- // Whenever the stream has data, add it to the queue
61
- audioBufferStream.on('data', (data) => {
62
- const floats = new Float32Array(new Uint8Array(data).buffer);
63
- queue.push(floats);
64
- });
61
+ /**
62
+ * Optional: Password for End-to-End Encryption.
63
+ * Set to null to disable, or set a string that matches other peers.
64
+ * @type {string|null}
65
+ */
66
+ const cipherPassword = null;
67
+
68
+ // ============================================
69
+ // Audio Sending Function
70
+ // ============================================
71
+
72
+ /**
73
+ * Decodes an audio file and sends it to the room in real-time.
74
+ * Splits the audio into 20ms chunks and sends them with precise timing.
75
+ *
76
+ * @param {object} media - The OdinMedia stream object
77
+ * @param {number} sampleRate - Target sample rate in Hz
78
+ * @param {number} numChannels - Number of channels (1=mono, 2=stereo)
79
+ */
80
+ async function sendMusic(media, sampleRate, numChannels) {
81
+ console.log("Loading and decoding audio file...");
65
82
 
66
- // Start a timer to send audio data at regular intervals
67
- const interval = setInterval(() => {
68
- if (queue.length > 0) {
69
- const chunk = queue.shift();
70
- media.sendAudioData(chunk);
71
- } else {
72
- // If there's no more data to send, stop the timer
73
- clearInterval(interval);
74
- audioBufferStream.end();
75
- console.log("Audio finished");
83
+ // Load and decode the MP3 file
84
+ const audioBuffer = await decode(fs.readFileSync(path.join(__dirname, 'santa.mp3')));
85
+ console.log(`Audio loaded: ${audioBuffer.duration.toFixed(2)}s, ${audioBuffer.sampleRate}Hz, ${audioBuffer.numberOfChannels} channels`);
86
+
87
+ // Convert to interleaved format if stereo
88
+ let audioData;
89
+ if (numChannels === 2) {
90
+ const left = audioBuffer.getChannelData(0);
91
+ const right = audioBuffer.getChannelData(1);
92
+ const interleaved = new Float32Array(left.length * 2);
93
+ for (let i = 0; i < left.length; i++) {
94
+ interleaved[i * 2] = left[i];
95
+ interleaved[i * 2 + 1] = right[i];
76
96
  }
77
- }, 20); // Send a chunk every 20ms
97
+ audioData = interleaved;
98
+ } else {
99
+ audioData = audioBuffer.getChannelData(0);
100
+ }
101
+
102
+ // Calculate chunk size for 20ms of audio
103
+ const chunkDurationMs = 20;
104
+ const samplesPerChunk = Math.floor(sampleRate * chunkDurationMs / 1000);
105
+ const floatsPerChunk = samplesPerChunk * numChannels;
106
+
107
+ // Split audio into 20ms chunks
108
+ const chunks = [];
109
+ for (let offset = 0; offset < audioData.length; offset += floatsPerChunk) {
110
+ const end = Math.min(offset + floatsPerChunk, audioData.length);
111
+ chunks.push(audioData.slice(offset, end));
112
+ }
113
+
114
+ console.log(`Sending ${chunks.length} chunks (${samplesPerChunk} samples/chunk at ${sampleRate}Hz)...`);
115
+
116
+ // Send chunks with precise timing to maintain real-time playback
117
+ const startTime = Date.now();
118
+ for (let i = 0; i < chunks.length; i++) {
119
+ // Send the audio data
120
+ media.sendAudioData(chunks[i]);
78
121
 
79
- audioBufferStream.write(audioBuffer);
122
+ // Calculate when the next chunk should be sent
123
+ const nextChunkTime = startTime + (i + 1) * chunkDurationMs;
124
+ const now = Date.now();
125
+ const waitTime = nextChunkTime - now;
126
+
127
+ // Wait if we're ahead of schedule
128
+ if (waitTime > 0 && i < chunks.length - 1) {
129
+ await new Promise(resolve => setTimeout(resolve, waitTime));
130
+ }
131
+ }
132
+
133
+ console.log(`Audio streaming complete. Sent ${chunks.length} chunks.`);
80
134
  }
81
135
 
82
- room.setPositionScale(25);
83
- room.updatePosition(0, 0, 0);
136
+ // ============================================
137
+ // Main Function
138
+ // ============================================
84
139
 
85
- // Create a media stream in the room - it will return an OdinMedia instance that we can use to send data to ODIN
86
- const media = room.createAudioStream(48000, 2);
87
- console.log(media);
88
- console.log("MEDIA-ID:", media.id);
140
+ async function main() {
141
+ console.log("=== ODIN NodeJS SDK - Audio Sending (Low-Level API) ===\n");
89
142
 
90
- // Start the stream and send the music to ODIN
91
- sendMusic(media).then(() => {
92
- console.log("Started sending audio");
93
- });
143
+ // Prepare user data to send with join
144
+ const userData = {
145
+ name: "Music Bot",
146
+ avatar: "https://avatars.dicebear.com/api/bottts/123.svg",
147
+ platform: "ODIN Node.js SDK",
148
+ version: "0.11.0"
149
+ };
150
+ const data = new TextEncoder().encode(JSON.stringify(userData));
151
+
152
+ // Step 1: Create ODIN client
153
+ console.log("1. Creating ODIN client...");
154
+ const client = new OdinClient();
155
+
156
+ // Step 2: Generate token from access key
157
+ console.log("2. Generating token from access key...");
158
+ const token = client.generateToken(accessKey, roomId, userId);
159
+ console.log(" Token generated.\n");
160
+
161
+ // Step 3: Create room
162
+ console.log("3. Creating room...");
163
+ const room = client.createRoom(token);
164
+
165
+ // Step 4: Set up E2EE cipher (optional)
166
+ if (cipherPassword) {
167
+ console.log("4. Setting up E2EE cipher...");
168
+ const cipher = new OdinCipher();
169
+ cipher.setPassword(new TextEncoder().encode(cipherPassword));
170
+ room.setCipher(cipher);
171
+ console.log(" E2EE enabled.\n");
172
+ } else {
173
+ console.log("4. E2EE disabled.\n");
174
+ }
175
+
176
+ // Set position (optional, for spatial audio)
177
+ room.setPositionScale(25);
178
+ room.updatePosition(0, 0, 0);
179
+
180
+ // Variable to track if audio has started (prevent duplicate handling)
181
+ let audioStarted = false;
94
182
 
95
- // Wait until the user presses a key to stop
96
- console.log("Press any key to stop");
97
- const stdin = process.stdin;
98
- stdin.resume();
99
- stdin.setEncoding( 'utf8' );
100
- stdin.on( 'data', function( key )
101
- {
102
- console.log("Shutting down");
183
+ // Listen for Joined event to get media IDs
184
+ room.onJoined(async (event) => {
185
+ console.log(`\n[Event] Joined`);
186
+ console.log(` Own Peer ID: ${event.ownPeerId}`);
187
+ console.log(` Available Media IDs: ${JSON.stringify(event.mediaIds)}`);
188
+
189
+ // Prevent duplicate audio sending
190
+ if (audioStarted) return;
191
+ audioStarted = true;
192
+
193
+ // Verify we have media IDs available
194
+ if (!event.mediaIds || event.mediaIds.length === 0) {
195
+ console.error(" ERROR: No media IDs available!");
196
+ return;
197
+ }
198
+
199
+ // Get the first available media ID
200
+ const mediaId = event.mediaIds[0];
201
+ console.log(` Using Media ID: ${mediaId}\n`);
202
+
203
+ // Step 5: Create audio stream with source sample rate
204
+ const sampleRate = 44100; // MP3 sample rate
205
+ const numChannels = 2; // Stereo
206
+ console.log("5. Creating audio stream...");
207
+ const media = room.createAudioStream(sampleRate, numChannels);
208
+ console.log(` Audio stream created (${sampleRate}Hz, ${numChannels === 2 ? 'stereo' : 'mono'}).`);
209
+
210
+ // Step 6: Set the server-assigned media ID (critical for SDK 1.8.2+)
211
+ console.log("6. Setting media ID...");
212
+ media.setMediaId(mediaId);
213
+ console.log(` Media ID set to ${mediaId}.`);
214
+
215
+ // Step 7: Send StartMedia RPC to notify server
216
+ // Format: [type, requestId, methodName, params]
217
+ console.log("7. Sending StartMedia RPC...");
218
+ const startMediaRpc = encode([0, 1, "StartMedia", {
219
+ media_id: mediaId,
220
+ properties: { kind: "audio" }
221
+ }]);
222
+ room.sendRpc(new Uint8Array(startMediaRpc));
223
+ console.log(" StartMedia RPC sent.");
224
+
225
+ // Step 8: Send the audio
226
+ console.log("8. Sending audio...");
227
+ await sendMusic(media, sampleRate, numChannels);
228
+ console.log(" Audio sending complete.\n");
229
+
230
+ // Wait for audio to finish transmitting
231
+ console.log("9. Waiting for transmission to complete...");
232
+ await new Promise(resolve => setTimeout(resolve, 5000));
233
+
234
+ // Step 10: Clean up
235
+ console.log("10. Closing audio stream...");
236
+ media.close();
237
+ console.log(" Audio stream closed.\n");
238
+ });
239
+
240
+ // Connection state handler
241
+ room.onConnectionStateChanged((event) => {
242
+ console.log(`[Event] ConnectionStateChanged: ${event.state}${event.message ? ` (${event.message})` : ''}`);
243
+ });
244
+
245
+ // Step: Join the room
246
+ console.log("Joining room...");
247
+ room.join("https://gateway.odin.4players.io", data);
248
+ console.log(" Join initiated.\n");
249
+
250
+ // Keep alive for audio transmission
251
+ console.log("Waiting 30 seconds for audio transmission...\n");
252
+ await new Promise(resolve => setTimeout(resolve, 30000));
253
+
254
+ // Close room
255
+ console.log("\nClosing room...");
103
256
  room.close();
257
+ console.log("Room closed.\n");
104
258
 
105
- process.exit();
106
- });
259
+ console.log("=== Test Complete ===");
260
+ process.exit(0);
261
+ }
107
262
 
263
+ // ============================================
264
+ // Entry Point
265
+ // ============================================
266
+
267
+ main().catch(err => {
268
+ console.error("Error:", err);
269
+ process.exit(1);
270
+ });