@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
package/README.md
CHANGED
|
@@ -1,69 +1,628 @@
|
|
|
1
|
-
# ODIN Node
|
|
1
|
+
# ODIN Node.js SDK
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@4players/odin-nodejs)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://4np.de/discord)
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
Native Node.js bindings for the [ODIN Voice SDK](https://github.com/4Players/odin-sdk). Build powerful voice chat applications, recording bots, AI integrations, and real-time audio processing tools.
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
📖 **[Full Documentation](https://docs.4players.io/voice/nodejs/)** | 💬 **[Discord Community](https://4np.de/discord)** | 🎮 **[4Players ODIN](https://www.4players.io/odin)**
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
for production use as there might be bugs, memory leaks or other issues.** But it's good enough for testing AI
|
|
12
|
-
integration, recording or other interesting use cases.
|
|
11
|
+
---
|
|
13
12
|
|
|
14
|
-
|
|
13
|
+
## Features
|
|
15
14
|
|
|
16
|
-
|
|
15
|
+
- 🎙️ **Real-time Voice Chat** - Low-latency voice communication
|
|
16
|
+
- 🔐 **End-to-End Encryption** - Built-in E2EE with OdinCipher
|
|
17
|
+
- 🤖 **Bot Integration** - Perfect for recording bots, AI assistants, and moderation tools
|
|
18
|
+
- 📊 **Raw Audio Access** - Get PCM audio data for processing, recording, or transcription
|
|
19
|
+
- 🌍 **Proximity Chat** - 3D positional audio support
|
|
20
|
+
- ⚡ **High Performance** - Native C++ bindings for maximum efficiency
|
|
21
|
+
- 📈 **Diagnostics** - Real-time connection and audio quality monitoring
|
|
17
22
|
|
|
18
|
-
|
|
19
|
-
works on NodeJS (to some extent) it is not as performant as the native bindings and also has some limitations.
|
|
20
|
-
For bots and other advanced use cases we provide this native Node JS SDK.
|
|
23
|
+
---
|
|
21
24
|
|
|
22
|
-
|
|
23
|
-
JavaScript and TypeScript for better separating doing so in native C/C++ is harder and error prone. Therefore we decided
|
|
24
|
-
to only use C/C++ and objects where required. For example, the WebSDK provides an `OdinPeer` object that wraps various
|
|
25
|
-
methods and properties. For bots and other NodeJS use cases you typically don't need that and therefore we decided to
|
|
26
|
-
not make things more complex than required. You typically only get a `peerId` and `mediaId` and that's it. But for most
|
|
27
|
-
use cases this is enough, and you are always free to build your own wrapper where required.
|
|
25
|
+
## Installation
|
|
28
26
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
it's super easy (and fast) to wrap the ODIN native SDK into NodeJS bindings.
|
|
27
|
+
```bash
|
|
28
|
+
npm install @4players/odin-nodejs
|
|
29
|
+
```
|
|
33
30
|
|
|
34
|
-
|
|
31
|
+
### Prerequisites
|
|
35
32
|
|
|
36
|
-
|
|
33
|
+
This SDK includes prebuilt binaries for:
|
|
34
|
+
- **macOS** (x86_64 and arm64)
|
|
35
|
+
- **Windows** (x86_64)
|
|
36
|
+
- **Linux** (x86_64)
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
`npm install` this module will be compiled, which means you may need to have a compiler installed.
|
|
38
|
+
For other platforms, you'll need a C++ compiler. See [node-gyp requirements](https://github.com/nodejs/node-gyp#installation).
|
|
40
39
|
|
|
41
|
-
|
|
42
|
-
- macOS (x86_64 and arm64)
|
|
43
|
-
- Windows (x86_64)
|
|
44
|
-
- Linux (x86_64)
|
|
40
|
+
---
|
|
45
41
|
|
|
46
|
-
|
|
47
|
-
to have GCC installed. More information can be found here: [node-gyp](https://github.com/nodejs/node-gyp).
|
|
42
|
+
## Quick Start
|
|
48
43
|
|
|
49
|
-
|
|
44
|
+
### 1. Get Your Access Key
|
|
50
45
|
|
|
51
|
-
|
|
46
|
+
Sign up at [4Players ODIN](https://www.4players.io/odin) to get your free access key.
|
|
52
47
|
|
|
53
|
-
|
|
54
|
-
|
|
48
|
+
### 2. Basic Connection Example
|
|
49
|
+
|
|
50
|
+
```javascript
|
|
51
|
+
import odin from '@4players/odin-nodejs';
|
|
52
|
+
const { OdinClient } = odin;
|
|
53
|
+
|
|
54
|
+
// Configuration - replace with your credentials
|
|
55
|
+
const accessKey = "__YOUR_ACCESS_KEY__";
|
|
56
|
+
const roomId = "my-room";
|
|
57
|
+
const userId = "user-123";
|
|
58
|
+
|
|
59
|
+
async function main() {
|
|
60
|
+
// Create client and generate token locally
|
|
61
|
+
const client = new OdinClient();
|
|
62
|
+
const token = client.generateToken(accessKey, roomId, userId);
|
|
63
|
+
|
|
64
|
+
// Create room using factory pattern
|
|
65
|
+
const room = client.createRoom(token);
|
|
66
|
+
|
|
67
|
+
// Set up event handlers
|
|
68
|
+
room.onJoined((event) => {
|
|
69
|
+
console.log(`Joined room: ${event.roomId}`);
|
|
70
|
+
console.log(`My peer ID: ${event.ownPeerId}`);
|
|
71
|
+
console.log(`Available media IDs: ${event.mediaIds}`);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
room.onPeerJoined((event) => {
|
|
75
|
+
console.log(`Peer joined: ${event.peerId}`);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
room.onPeerLeft((event) => {
|
|
79
|
+
console.log(`Peer left: ${event.peerId}`);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Join the room
|
|
83
|
+
room.join("https://gateway.odin.4players.io");
|
|
84
|
+
|
|
85
|
+
// Keep connection alive
|
|
86
|
+
process.on('SIGINT', () => {
|
|
87
|
+
room.close();
|
|
88
|
+
process.exit(0);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
main();
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Event Handlers
|
|
98
|
+
|
|
99
|
+
The SDK provides typed event handlers for easy integration:
|
|
100
|
+
|
|
101
|
+
```javascript
|
|
102
|
+
// Connection events
|
|
103
|
+
room.onConnectionStateChanged((event) => {
|
|
104
|
+
console.log(`State: ${event.state}`); // Connecting, Joined, Disconnected, etc.
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
room.onJoined((event) => {
|
|
108
|
+
// { roomId, ownPeerId, room, mediaIds }
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
room.onLeft((event) => {
|
|
112
|
+
// { reason }
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Peer events
|
|
116
|
+
room.onPeerJoined((event) => {
|
|
117
|
+
// { peerId, userId, userData, peer }
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
room.onPeerLeft((event) => {
|
|
121
|
+
// { peerId }
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Media events
|
|
125
|
+
room.onMediaStarted((event) => {
|
|
126
|
+
// { peerId, media }
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
room.onMediaStopped((event) => {
|
|
130
|
+
// { peerId, mediaId }
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
room.onMediaActivity((event) => {
|
|
134
|
+
// { peerId, mediaId, state } - Voice Activity Detection
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Messages
|
|
138
|
+
room.onMessageReceived((event) => {
|
|
139
|
+
// { senderPeerId, message }
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Audio data (for recording/processing)
|
|
143
|
+
room.onAudioDataReceived((data) => {
|
|
144
|
+
// { peerId, mediaId, samples16, samples32 }
|
|
145
|
+
});
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Audio Recording Example
|
|
151
|
+
|
|
152
|
+
Record audio from peers to WAV files:
|
|
153
|
+
|
|
154
|
+
```javascript
|
|
155
|
+
import odin from '@4players/odin-nodejs';
|
|
156
|
+
import wav from 'wav';
|
|
157
|
+
|
|
158
|
+
const { OdinClient } = odin;
|
|
159
|
+
|
|
160
|
+
// Configuration
|
|
161
|
+
const accessKey = "__YOUR_ACCESS_KEY__";
|
|
162
|
+
const roomId = "my-room";
|
|
163
|
+
const userId = "RecorderBot";
|
|
164
|
+
|
|
165
|
+
const recordings = {};
|
|
166
|
+
|
|
167
|
+
async function main() {
|
|
168
|
+
const client = new OdinClient();
|
|
169
|
+
const token = client.generateToken(accessKey, roomId, userId);
|
|
170
|
+
const room = client.createRoom(token);
|
|
171
|
+
|
|
172
|
+
room.onAudioDataReceived((data) => {
|
|
173
|
+
const { mediaId, peerId, samples16 } = data;
|
|
174
|
+
|
|
175
|
+
// Create recording file if needed
|
|
176
|
+
if (!recordings[mediaId]) {
|
|
177
|
+
recordings[mediaId] = new wav.FileWriter(`recording_${peerId}.wav`, {
|
|
178
|
+
channels: 2,
|
|
179
|
+
sampleRate: 48000,
|
|
180
|
+
bitDepth: 16
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Write audio samples
|
|
185
|
+
const buffer = Buffer.from(samples16.buffer, samples16.byteOffset, samples16.byteLength);
|
|
186
|
+
recordings[mediaId].write(buffer);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
room.onMediaStopped((event) => {
|
|
190
|
+
if (recordings[event.mediaId]) {
|
|
191
|
+
recordings[event.mediaId].end();
|
|
192
|
+
delete recordings[event.mediaId];
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
room.join("https://gateway.odin.4players.io");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
main();
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Sending Audio
|
|
205
|
+
|
|
206
|
+
The SDK provides two approaches for sending audio: a **high-level API** for convenience and a **low-level API** for full control.
|
|
207
|
+
|
|
208
|
+
### High-Level API (Recommended)
|
|
209
|
+
|
|
210
|
+
The high-level API handles all the complexity automatically - media ID allocation, StartMedia RPC, and timing:
|
|
211
|
+
|
|
212
|
+
```javascript
|
|
213
|
+
import odin from '@4players/odin-nodejs';
|
|
214
|
+
const { OdinClient } = odin;
|
|
215
|
+
|
|
216
|
+
// Configuration
|
|
217
|
+
const accessKey = "__YOUR_ACCESS_KEY__";
|
|
218
|
+
const roomId = "my-room";
|
|
219
|
+
const userId = "AudioBot";
|
|
220
|
+
|
|
221
|
+
async function main() {
|
|
222
|
+
const client = new OdinClient();
|
|
223
|
+
const token = client.generateToken(accessKey, roomId, userId);
|
|
224
|
+
const room = client.createRoom(token);
|
|
225
|
+
|
|
226
|
+
// Wait for room join
|
|
227
|
+
const joinPromise = new Promise(resolve => room.onJoined(resolve));
|
|
228
|
+
room.join("https://gateway.odin.4players.io");
|
|
229
|
+
await joinPromise;
|
|
230
|
+
|
|
231
|
+
// Create audio stream and send audio with one line!
|
|
232
|
+
const media = room.createAudioStream(44100, 2);
|
|
233
|
+
|
|
234
|
+
// Send an MP3 file (auto-decodes and streams with correct timing)
|
|
235
|
+
await media.sendMP3('./music.mp3');
|
|
236
|
+
|
|
237
|
+
// Or send a WAV file
|
|
238
|
+
await media.sendWAV('./audio.wav');
|
|
239
|
+
|
|
240
|
+
// Or send a decoded AudioBuffer
|
|
241
|
+
// await media.sendBuffer(audioBuffer);
|
|
242
|
+
|
|
243
|
+
media.close();
|
|
244
|
+
room.close();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
main();
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Low-Level API
|
|
251
|
+
|
|
252
|
+
For full control over audio transmission, use the low-level API:
|
|
253
|
+
|
|
254
|
+
```javascript
|
|
255
|
+
import odin from '@4players/odin-nodejs';
|
|
256
|
+
const { OdinClient } = odin;
|
|
257
|
+
import { encode } from '@msgpack/msgpack';
|
|
258
|
+
|
|
259
|
+
// Configuration
|
|
260
|
+
const accessKey = "__YOUR_ACCESS_KEY__";
|
|
261
|
+
const roomId = "my-room";
|
|
262
|
+
const userId = "AudioBot";
|
|
263
|
+
|
|
264
|
+
async function main() {
|
|
265
|
+
const client = new OdinClient();
|
|
266
|
+
const token = client.generateToken(accessKey, roomId, userId);
|
|
267
|
+
const room = client.createRoom(token);
|
|
268
|
+
|
|
269
|
+
room.onJoined(async (event) => {
|
|
270
|
+
// 1. Get media ID from the event
|
|
271
|
+
const mediaId = event.mediaIds[0];
|
|
272
|
+
|
|
273
|
+
// 2. Create audio stream
|
|
274
|
+
const media = room.createAudioStream(48000, 2);
|
|
275
|
+
|
|
276
|
+
// 3. Set the server-assigned media ID
|
|
277
|
+
media.setMediaId(mediaId);
|
|
278
|
+
|
|
279
|
+
// 4. Send StartMedia RPC to notify server
|
|
280
|
+
const rpc = encode([0, 1, "StartMedia", {
|
|
281
|
+
media_id: mediaId,
|
|
282
|
+
properties: { kind: "audio" }
|
|
283
|
+
}]);
|
|
284
|
+
room.sendRpc(new Uint8Array(rpc));
|
|
285
|
+
|
|
286
|
+
// 5. Send audio data in 20ms chunks
|
|
287
|
+
const chunkDurationMs = 20;
|
|
288
|
+
const samplesPerChunk = Math.floor(48000 * chunkDurationMs / 1000) * 2;
|
|
289
|
+
|
|
290
|
+
// Your audio data as Float32Array (interleaved stereo, range [-1, 1])
|
|
291
|
+
const audioChunk = new Float32Array(samplesPerChunk);
|
|
292
|
+
// ... fill with audio samples ...
|
|
293
|
+
media.sendAudioData(audioChunk);
|
|
294
|
+
|
|
295
|
+
// 6. When done, close
|
|
296
|
+
media.close();
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
room.join("https://gateway.odin.4players.io");
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
main();
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
See [tests/sending-audio/](tests/sending-audio/) for complete examples of both APIs.
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
## End-to-End Encryption (E2EE)
|
|
310
|
+
|
|
311
|
+
Enable encryption for secure voice communication:
|
|
312
|
+
|
|
313
|
+
```javascript
|
|
314
|
+
import odin from '@4players/odin-nodejs';
|
|
315
|
+
const { OdinClient, OdinCipher } = odin;
|
|
316
|
+
|
|
317
|
+
const client = new OdinClient();
|
|
318
|
+
const token = client.generateToken(accessKey, roomId, userId);
|
|
319
|
+
const room = client.createRoom(token);
|
|
320
|
+
|
|
321
|
+
// Create and configure cipher
|
|
322
|
+
const cipher = new OdinCipher();
|
|
323
|
+
cipher.setPassword(new TextEncoder().encode("shared-secret-password"));
|
|
324
|
+
|
|
325
|
+
// Apply cipher to room
|
|
326
|
+
room.setCipher(cipher);
|
|
327
|
+
|
|
328
|
+
room.join("https://gateway.odin.4players.io");
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
> ⚠️ All participants in a room must use the same cipher password to communicate.
|
|
332
|
+
|
|
333
|
+
### Verifying Peer Encryption Status
|
|
334
|
+
|
|
335
|
+
```javascript
|
|
336
|
+
// Check if a peer's encryption matches ours
|
|
337
|
+
const status = cipher.getPeerStatus(peerId);
|
|
338
|
+
console.log(`Peer ${peerId} encryption: ${status}`);
|
|
339
|
+
// Possible values: "encrypted", "mismatch", "unencrypted", "unknown"
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
---
|
|
343
|
+
|
|
344
|
+
## Proximity Chat (3D Audio)
|
|
345
|
+
|
|
346
|
+
Enable distance-based audio for spatial applications:
|
|
347
|
+
|
|
348
|
+
```javascript
|
|
349
|
+
room.onJoined(() => {
|
|
350
|
+
// Set position scale (1 unit = 1 meter)
|
|
351
|
+
room.setPositionScale(1.0);
|
|
352
|
+
|
|
353
|
+
// Update your position
|
|
354
|
+
room.updatePosition(10.0, 0.0, 5.0); // x, y, z
|
|
355
|
+
});
|
|
55
356
|
```
|
|
56
357
|
|
|
57
|
-
|
|
58
|
-
|
|
358
|
+
---
|
|
359
|
+
|
|
360
|
+
## Connection Diagnostics
|
|
361
|
+
|
|
362
|
+
Monitor connection quality and troubleshoot issues:
|
|
363
|
+
|
|
364
|
+
```javascript
|
|
365
|
+
room.onJoined(() => {
|
|
366
|
+
// Get connection identifier
|
|
367
|
+
const connectionId = room.getConnectionId();
|
|
368
|
+
console.log(`Connection ID: ${connectionId}`);
|
|
369
|
+
|
|
370
|
+
// Get detailed connection statistics
|
|
371
|
+
const stats = room.getConnectionStats();
|
|
372
|
+
if (stats) {
|
|
373
|
+
console.log(`RTT: ${stats.rtt.toFixed(2)} ms`);
|
|
374
|
+
console.log(`TX Loss: ${(stats.udpTxLoss * 100).toFixed(2)}%`);
|
|
375
|
+
console.log(`RX Loss: ${(stats.udpRxLoss * 100).toFixed(2)}%`);
|
|
376
|
+
console.log(`TX Bytes: ${stats.udpTxBytes}`);
|
|
377
|
+
console.log(`RX Bytes: ${stats.udpRxBytes}`);
|
|
378
|
+
console.log(`Congestion Events: ${stats.congestionEvents}`);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Get jitter statistics for an audio stream
|
|
382
|
+
const jitterStats = room.getJitterStats(mediaId);
|
|
383
|
+
if (jitterStats) {
|
|
384
|
+
console.log(`Packets Total: ${jitterStats.packetsTotal}`);
|
|
385
|
+
console.log(`Packets Lost: ${jitterStats.packetsLost}`);
|
|
386
|
+
console.log(`Packets Too Late: ${jitterStats.packetsArrivedTooLate}`);
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
---
|
|
392
|
+
|
|
393
|
+
## API Reference
|
|
394
|
+
|
|
395
|
+
### OdinClient
|
|
396
|
+
|
|
397
|
+
| Method | Description |
|
|
398
|
+
|--------|-------------|
|
|
399
|
+
| `generateToken(accessKey, roomId, userId)` | Generate a room token locally |
|
|
400
|
+
| `createRoom(token)` | Create a room instance (recommended) |
|
|
401
|
+
| `createRoomWithToken(token)` | Alias for createRoom |
|
|
402
|
+
|
|
403
|
+
### OdinRoom
|
|
404
|
+
|
|
405
|
+
| Method | Description |
|
|
406
|
+
|--------|-------------|
|
|
407
|
+
| `join(gateway, userData?)` | Connect to the room |
|
|
408
|
+
| `close()` | Disconnect from the room |
|
|
409
|
+
| `sendMessage(data, peerIds?)` | Send a message to peers |
|
|
410
|
+
| `updatePosition(x, y, z)` | Update 3D position |
|
|
411
|
+
| `setPositionScale(scale)` | Set position scale factor |
|
|
412
|
+
| `setCipher(cipher)` | Enable E2EE |
|
|
413
|
+
| `createAudioStream(sampleRate, channels)` | Create audio output stream |
|
|
414
|
+
| `getConnectionId()` | Get connection identifier |
|
|
415
|
+
| `getConnectionStats()` | Get connection quality metrics |
|
|
416
|
+
| `getJitterStats(mediaId)` | Get audio jitter metrics |
|
|
417
|
+
|
|
418
|
+
### OdinRoom Properties
|
|
419
|
+
|
|
420
|
+
| Property | Type | Description |
|
|
421
|
+
|----------|------|-------------|
|
|
422
|
+
| `ownPeerId` | `number` | Your peer ID |
|
|
423
|
+
| `connected` | `boolean` | Connection status |
|
|
424
|
+
| `availableMediaIds` | `number[]` | Available media IDs for audio streams |
|
|
425
|
+
|
|
426
|
+
### OdinMedia (Audio Stream)
|
|
427
|
+
|
|
428
|
+
| Method | Description |
|
|
429
|
+
|--------|-------------|
|
|
430
|
+
| `setMediaId(mediaId)` | Set server-assigned media ID |
|
|
431
|
+
| `close()` | Release the stream |
|
|
432
|
+
| `sendAudioData(samples)` | Send raw audio samples |
|
|
433
|
+
| `sendMP3(filePath)` | Stream an MP3 file (convenience) |
|
|
434
|
+
| `sendWAV(filePath)` | Stream a WAV file (convenience) |
|
|
435
|
+
| `sendBuffer(audioBuffer)` | Stream AudioBuffer (convenience) |
|
|
436
|
+
|
|
437
|
+
### OdinCipher (E2EE)
|
|
438
|
+
|
|
439
|
+
| Method | Description |
|
|
440
|
+
|--------|-------------|
|
|
441
|
+
| `setPassword(password)` | Set encryption password |
|
|
442
|
+
| `getPeerStatus(peerId)` | Get peer's encryption status |
|
|
59
443
|
|
|
60
|
-
|
|
444
|
+
### Events
|
|
61
445
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
446
|
+
| Event | Payload |
|
|
447
|
+
|-------|---------|
|
|
448
|
+
| `ConnectionStateChanged` | `{ state, message }` |
|
|
449
|
+
| `Joined` | `{ roomId, ownPeerId, room, mediaIds }` |
|
|
450
|
+
| `Left` | `{ reason }` |
|
|
451
|
+
| `PeerJoined` | `{ peerId, userId, userData, peer }` |
|
|
452
|
+
| `PeerLeft` | `{ peerId }` |
|
|
453
|
+
| `MediaStarted` | `{ peerId, media }` |
|
|
454
|
+
| `MediaStopped` | `{ peerId, mediaId }` |
|
|
455
|
+
| `MediaActivity` | `{ peerId, mediaId, state }` |
|
|
456
|
+
| `MessageReceived` | `{ senderPeerId, message }` |
|
|
457
|
+
| `AudioDataReceived` | `{ peerId, mediaId, samples16, samples32 }` |
|
|
458
|
+
|
|
459
|
+
---
|
|
460
|
+
|
|
461
|
+
## Comparison with Web SDK
|
|
462
|
+
|
|
463
|
+
| Feature | Node.js SDK | Web SDK |
|
|
464
|
+
|---------|-------------|---------|
|
|
465
|
+
| Platform | Node.js (server) | Browser |
|
|
466
|
+
| Performance | Native C++ | WebRTC/JavaScript |
|
|
467
|
+
| Raw Audio Access | ✅ Full PCM data | ⚠️ Web Audio API |
|
|
468
|
+
| Use Cases | Bots, recording, AI | Client apps |
|
|
469
|
+
| E2EE | ✅ OdinCipher | ✅ OdinCipher |
|
|
470
|
+
|
|
471
|
+
The Node.js SDK is optimized for server-side use cases like:
|
|
472
|
+
- 🎙️ Audio recording bots
|
|
473
|
+
- 🤖 AI-powered voice assistants
|
|
474
|
+
- 📝 Speech-to-text transcription
|
|
475
|
+
- 🛡️ Content moderation
|
|
476
|
+
- 🔊 Audio processing pipelines
|
|
477
|
+
|
|
478
|
+
---
|
|
65
479
|
|
|
66
480
|
## Examples
|
|
67
481
|
|
|
68
|
-
|
|
482
|
+
Check the `tests/` folder for complete examples:
|
|
483
|
+
|
|
484
|
+
- **[connection-test](tests/connection-test/)** - Basic connection, events, and diagnostics
|
|
485
|
+
- **[audio-recording](tests/audio-recording/)** - Recording peer audio to WAV files
|
|
486
|
+
- **[sending-audio](tests/sending-audio/)** - Sending audio with both high-level and low-level APIs
|
|
487
|
+
|
|
488
|
+
---
|
|
489
|
+
|
|
490
|
+
## Troubleshooting
|
|
491
|
+
|
|
492
|
+
### Build Errors
|
|
493
|
+
|
|
494
|
+
If you encounter build errors, ensure you have the required tools:
|
|
495
|
+
|
|
496
|
+
```bash
|
|
497
|
+
# macOS
|
|
498
|
+
xcode-select --install
|
|
499
|
+
|
|
500
|
+
# Ubuntu/Debian
|
|
501
|
+
sudo apt-get install build-essential python3
|
|
502
|
+
|
|
503
|
+
# Windows
|
|
504
|
+
npm install --global windows-build-tools
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
### macOS Security Warnings
|
|
508
|
+
|
|
509
|
+
If you see "code signature not valid" errors:
|
|
510
|
+
|
|
511
|
+
```bash
|
|
512
|
+
cd node_modules/@4players/odin-nodejs/build/Debug
|
|
513
|
+
xattr -cr *.dylib
|
|
514
|
+
codesign -f -s - *.dylib
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
### Connection Issues
|
|
518
|
+
|
|
519
|
+
1. Verify your access key is correct
|
|
520
|
+
2. Check your network allows WebSocket connections
|
|
521
|
+
3. Ensure the token hasn't expired
|
|
522
|
+
|
|
523
|
+
---
|
|
524
|
+
|
|
525
|
+
## Development
|
|
526
|
+
|
|
527
|
+
### Building for Other Platforms
|
|
528
|
+
|
|
529
|
+
This package includes prebuilt binaries for common platforms (macOS x64/arm64, Windows x64, Linux x64). If you need to build for a different platform or architecture, follow these steps:
|
|
530
|
+
|
|
531
|
+
#### 1. Install Build Requirements
|
|
532
|
+
|
|
533
|
+
You'll need a C++ compiler toolchain:
|
|
534
|
+
|
|
535
|
+
```bash
|
|
536
|
+
# macOS
|
|
537
|
+
xcode-select --install
|
|
538
|
+
|
|
539
|
+
# Ubuntu/Debian
|
|
540
|
+
sudo apt-get install build-essential python3
|
|
541
|
+
|
|
542
|
+
# Windows
|
|
543
|
+
npm install --global windows-build-tools
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
#### 2. Download ODIN SDK Libraries
|
|
547
|
+
|
|
548
|
+
Download the ODIN SDK libraries from the [official releases](https://github.com/4Players/odin-sdk/releases/tag/v1.8.2):
|
|
549
|
+
|
|
550
|
+
1. Download the appropriate archive for your platform from the release assets
|
|
551
|
+
2. Extract the libraries to the correct location:
|
|
552
|
+
|
|
553
|
+
| Platform | Architecture | Target Directory |
|
|
554
|
+
|----------|--------------|------------------|
|
|
555
|
+
| Linux | x64 | `libs/bin/linux/x64/` |
|
|
556
|
+
| Linux | arm64 | `libs/bin/linux/arm64/` |
|
|
557
|
+
| Linux | ia32 | `libs/bin/linux/ia32/` |
|
|
558
|
+
| macOS | Universal | `libs/bin/macos/universal/` |
|
|
559
|
+
| Windows | x64 | `libs/bin/windows/x64/` |
|
|
560
|
+
| Windows | ia32 | `libs/bin/windows/ia32/` |
|
|
561
|
+
|
|
562
|
+
The SDK archive contains these library files:
|
|
563
|
+
- **Linux**: `libodin_static.a`, `libodin.so`, `libodin_crypto_static.a`, `libodin_crypto.so`
|
|
564
|
+
- **macOS**: `libodin.dylib`, `libodin_crypto.dylib`, `libodin_static.a`, `libodin_crypto_static.a`
|
|
565
|
+
- **Windows**: `odin_static.lib`, `odin.dll`, `odin_crypto_static.lib`, `odin_crypto.dll`
|
|
566
|
+
|
|
567
|
+
#### 3. Build the Native Module
|
|
568
|
+
|
|
569
|
+
```bash
|
|
570
|
+
# Build in debug mode
|
|
571
|
+
npm run build:debug
|
|
572
|
+
|
|
573
|
+
# Build in release mode
|
|
574
|
+
npm run build:release
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
#### 4. Verify the Build
|
|
578
|
+
|
|
579
|
+
```bash
|
|
580
|
+
node -e "const odin = require('./index.cjs'); console.log('ODIN SDK loaded:', !!odin.OdinClient);"
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
### Project Structure
|
|
584
|
+
|
|
585
|
+
```
|
|
586
|
+
├── cppsrc/ # C++ native bindings source code
|
|
587
|
+
├── libs/
|
|
588
|
+
│ ├── bin/ # ODIN SDK binaries (all platforms)
|
|
589
|
+
│ │ ├── linux/ # Linux binaries (x64, arm64, ia32)
|
|
590
|
+
│ │ ├── macos/ # macOS binaries (arm64, x64, universal)
|
|
591
|
+
│ │ └── windows/ # Windows binaries (x64, ia32)
|
|
592
|
+
│ └── include/ # ODIN SDK headers (odin.h, odin_crypto.h)
|
|
593
|
+
├── index.cjs # JavaScript wrapper
|
|
594
|
+
├── *.d.ts # TypeScript type definitions
|
|
595
|
+
└── tests/ # Example scripts
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
---
|
|
599
|
+
|
|
600
|
+
## Contributing
|
|
601
|
+
|
|
602
|
+
We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
|
|
603
|
+
|
|
604
|
+
1. Fork the repository
|
|
605
|
+
2. Create a feature branch
|
|
606
|
+
3. Make your changes
|
|
607
|
+
4. Submit a pull request
|
|
608
|
+
|
|
609
|
+
---
|
|
610
|
+
|
|
611
|
+
## Support
|
|
612
|
+
|
|
613
|
+
- 📖 **Documentation**: [docs.4players.io](https://docs.4players.io/voice/nodejs/)
|
|
614
|
+
- 💬 **Discord**: [Join our community](https://4np.de/discord)
|
|
615
|
+
- 📧 **Email**: support@4players.io
|
|
616
|
+
- 🐛 **Issues**: [GitHub Issues](https://github.com/4Players/odin-nodejs/issues)
|
|
617
|
+
|
|
618
|
+
---
|
|
619
|
+
|
|
620
|
+
## License
|
|
621
|
+
|
|
622
|
+
MIT License - see [LICENSE](LICENSE) for details.
|
|
623
|
+
|
|
624
|
+
---
|
|
69
625
|
|
|
626
|
+
<p align="center">
|
|
627
|
+
Made with ❤️ by <a href="https://www.4players.io">4Players GmbH</a>
|
|
628
|
+
</p>
|