@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,133 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Post-build script for ODIN NodeJS bindings.
5
+ *
6
+ * On macOS:
7
+ * - Copies dynamic libraries (dylibs) to the build output directory
8
+ * - Ad-hoc signs them to prevent Gatekeeper from blocking
9
+ * - Creates prebuilds directory for npm link compatibility
10
+ *
11
+ * Usage:
12
+ * node scripts/postbuild.cjs [debug]
13
+ */
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+ const { execSync } = require('child_process');
18
+
19
+ const args = process.argv.slice(2);
20
+ const isDebug = args.includes('debug');
21
+ const buildDir = isDebug ? 'build/Debug' : 'build/Release';
22
+
23
+ const projectRoot = path.resolve(__dirname, '..');
24
+ const buildPath = path.join(projectRoot, buildDir);
25
+
26
+ // Platform/arch for prebuilds directory naming
27
+ const platform = process.platform;
28
+ const arch = process.arch;
29
+
30
+ console.log(`[postbuild] Platform: ${platform}, Arch: ${arch}`);
31
+ console.log(`[postbuild] Build mode: ${isDebug ? 'Debug' : 'Release'}`);
32
+ console.log(`[postbuild] Build path: ${buildPath}`);
33
+
34
+ /**
35
+ * Copy dylibs to a target directory and sign them
36
+ */
37
+ function copyAndSignDylibs(targetDir, dylibs, depsDir) {
38
+ // Ensure target directory exists
39
+ fs.mkdirSync(targetDir, { recursive: true });
40
+
41
+ // Copy dylibs
42
+ for (const dylib of dylibs) {
43
+ const src = path.join(depsDir, dylib);
44
+ const dest = path.join(targetDir, dylib);
45
+
46
+ if (!fs.existsSync(src)) {
47
+ console.error(`[postbuild] Source library not found: ${src}`);
48
+ process.exit(1);
49
+ }
50
+
51
+ console.log(`[postbuild] Copying ${dylib} to ${path.relative(projectRoot, targetDir)}/`);
52
+ fs.copyFileSync(src, dest);
53
+ }
54
+
55
+ // Ad-hoc sign the dylibs
56
+ console.log(`[postbuild] Ad-hoc signing dylibs in ${path.relative(projectRoot, targetDir)}/...`);
57
+ try {
58
+ execSync(`codesign -f -s - *.dylib`, {
59
+ cwd: targetDir,
60
+ stdio: 'inherit'
61
+ });
62
+ } catch (error) {
63
+ console.error(`[postbuild] Warning: codesign failed. Libraries may be blocked by Gatekeeper.`);
64
+ console.error(error.message);
65
+ }
66
+ }
67
+
68
+ if (platform === 'darwin') {
69
+ const depsDir = path.join(projectRoot, 'libs', 'bin', 'macos', 'universal');
70
+ const dylibs = ['libodin.dylib', 'libodin_crypto.dylib'];
71
+
72
+ // Ensure build directory exists
73
+ if (!fs.existsSync(buildPath)) {
74
+ console.error(`[postbuild] Build directory does not exist: ${buildPath}`);
75
+ process.exit(1);
76
+ }
77
+
78
+ // 1. Copy to build output directory
79
+ copyAndSignDylibs(buildPath, dylibs, depsDir);
80
+
81
+ // 2. Copy to prebuilds directory for npm link compatibility
82
+ // node-gyp-build looks for: prebuilds/{platform}-{arch}/
83
+ const prebuildsDir = path.join(projectRoot, 'prebuilds', `${platform}-${arch}`);
84
+ console.log(`[postbuild] Creating prebuilds for npm link compatibility...`);
85
+
86
+ // Copy the .node file to prebuilds
87
+ const nodeFile = 'odin.node';
88
+ const nodeSrc = path.join(buildPath, nodeFile);
89
+ if (fs.existsSync(nodeSrc)) {
90
+ fs.mkdirSync(prebuildsDir, { recursive: true });
91
+ const nodeDest = path.join(prebuildsDir, nodeFile);
92
+ console.log(`[postbuild] Copying ${nodeFile} to prebuilds/${platform}-${arch}/`);
93
+ fs.copyFileSync(nodeSrc, nodeDest);
94
+
95
+ // Copy dylibs to prebuilds too
96
+ copyAndSignDylibs(prebuildsDir, dylibs, depsDir);
97
+ } else {
98
+ console.log(`[postbuild] Note: ${nodeFile} not found in build output, skipping prebuilds.`);
99
+ }
100
+
101
+ console.log(`[postbuild] macOS post-build complete.`);
102
+ } else if (platform === 'linux') {
103
+ // For Linux, copy .so files if using dynamic linking
104
+ const depsDir = path.join(projectRoot, 'libs', 'bin', 'linux', arch);
105
+ const prebuildsDir = path.join(projectRoot, 'prebuilds', `${platform}-${arch}`);
106
+
107
+ // Copy .node file to prebuilds if it exists
108
+ const nodeFile = 'odin.node';
109
+ const nodeSrc = path.join(buildPath, nodeFile);
110
+ if (fs.existsSync(nodeSrc) && fs.existsSync(buildPath)) {
111
+ fs.mkdirSync(prebuildsDir, { recursive: true });
112
+ console.log(`[postbuild] Copying ${nodeFile} to prebuilds/${platform}-${arch}/`);
113
+ fs.copyFileSync(nodeSrc, path.join(prebuildsDir, nodeFile));
114
+ }
115
+
116
+ console.log(`[postbuild] Linux post-build complete (static linking, no dylib copy needed).`);
117
+ } else if (platform === 'win32') {
118
+ // For Windows, copy .node file to prebuilds
119
+ const prebuildsDir = path.join(projectRoot, 'prebuilds', `${platform}-${arch}`);
120
+
121
+ const nodeFile = 'odin.node';
122
+ const nodeSrc = path.join(buildPath, nodeFile);
123
+ if (fs.existsSync(nodeSrc) && fs.existsSync(buildPath)) {
124
+ fs.mkdirSync(prebuildsDir, { recursive: true });
125
+ console.log(`[postbuild] Copying ${nodeFile} to prebuilds/${platform}-${arch}/`);
126
+ fs.copyFileSync(nodeSrc, path.join(prebuildsDir, nodeFile));
127
+ }
128
+
129
+ console.log(`[postbuild] Windows post-build complete (static linking, no dll copy needed).`);
130
+ } else {
131
+ console.log(`[postbuild] Unknown platform: ${platform}`);
132
+ }
133
+
@@ -1,24 +1,109 @@
1
- # ODIN Node JS Sample "Audio Recording"
1
+ # ODIN Audio Recording Bot Example
2
2
 
3
- This sample demonstrates how to use the ODIN SDK to record audio from other users and sending these to OpenAI for
4
- transcription. It also features basic "Bot" functionality like sending a message to the chat.
3
+ This example demonstrates how to create a recording bot that captures audio from other peers in an ODIN room and saves it as WAV files.
4
+
5
+ ## Features
6
+
7
+ - **Automatic Recording**: Starts recording when peers begin speaking
8
+ - **WAV Output**: Saves recordings as standard WAV files (48kHz, stereo, 16-bit)
9
+ - **Peer Tracking**: Names recordings based on peer identity
10
+ - **Graceful Shutdown**: Properly finalizes recordings on exit
11
+ - **E2EE Support**: Optional end-to-end encryption
5
12
 
6
13
  ## Prerequisites
7
14
 
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.
15
+ 1. **ODIN Access Key**: Get one for free at [4Players ODIN](https://docs.4players.io/voice/introduction/access-keys/)
16
+ 2. **wav Package**: Already included in package.json dependencies
17
+
18
+ ## Configuration
19
+
20
+ Edit `index.js` and replace the placeholder values:
10
21
 
11
- This sample also requires an OpenAI API Key for transcription. You can get one for free [here](https://beta.openai.com/).
22
+ ```javascript
23
+ const accessKey = "__YOUR_ACCESS_KEY__"; // Your ODIN access key
24
+ const roomId = "__YOUR_ROOM_ID__"; // Room to record
25
+ const userId = "RecorderBot-123"; // Bot's user ID
26
+ ```
27
+
28
+ To enable E2EE (must match other peers):
12
29
 
13
- ## Getting Started
30
+ ```javascript
31
+ const cipherPassword = "shared-secret";
32
+ ```
14
33
 
15
- Run the script with the following command:
34
+ ## Running the Bot
16
35
 
17
36
  ```bash
37
+ cd tests/audio-recording
18
38
  node index.js
19
39
  ```
20
40
 
21
- It will connect to the room `Lobby` using your access key and will send a "Hello World" message to the chat. It will
22
- then wait for users start to talk and will record their audio into a file. The file name will be the user's name with a
23
- timestamp and will be a WAV file. 2 seconds after the user stops talking, the recording will be stopped and the file
24
- will be sent to OpenAI for transcription.
41
+ Press `Ctrl+C` to stop recording and save all files.
42
+
43
+ ## How It Works
44
+
45
+ 1. **Connects** to the specified ODIN room
46
+ 2. **Listens** for audio data from all peers
47
+ 3. **Creates** a new WAV file when a peer starts speaking
48
+ 4. **Writes** audio samples to the file in real-time
49
+ 5. **Finalizes** recordings when the peer stops or disconnects
50
+
51
+ ## Output Files
52
+
53
+ Recordings are saved in the current directory with the naming pattern:
54
+
55
+ ```
56
+ recording_{peerName}_media{id}_{timestamp}.wav
57
+ ```
58
+
59
+ Example: `recording_Alice_media123_2026-01-14T12-30-45-000Z.wav`
60
+
61
+ ### Audio Format
62
+
63
+ - **Sample Rate**: 48,000 Hz (ODIN native)
64
+ - **Channels**: 2 (stereo, interleaved)
65
+ - **Bit Depth**: 16-bit signed integer
66
+
67
+ ## Example Output
68
+
69
+ ```
70
+ === ODIN Audio Recording Bot ===
71
+
72
+ 1. Creating ODIN client...
73
+ 2. Generating token from access key...
74
+ Token generated successfully.
75
+
76
+ 3. Creating room...
77
+ 4. E2EE disabled (no password configured).
78
+
79
+ 5. Joining room...
80
+ Join initiated.
81
+
82
+ Recording bot is running. Press Ctrl+C to stop.
83
+
84
+ [Connection] State: Connecting
85
+ [Connection] State: Joined
86
+ [Room] Joined! Room ID: my-room, Own Peer ID: 12345
87
+ [Peer] Joined: Alice (ID: 67890)
88
+ [Media] Started: Peer 67890, Media ID: 1
89
+ [Recording] Started: ./recording_Alice_media1_2026-01-14T12-30-45-000Z.wav
90
+
91
+ [Media] Stopped: Peer 67890, Media ID: 1
92
+ [Recording] Saved: ./recording_Alice_media1_2026-01-14T12-30-45-000Z.wav (480000 samples)
93
+
94
+ ^C
95
+ [Shutdown] Stopping...
96
+ [Shutdown] Room closed.
97
+ ```
98
+
99
+ ## Use Cases
100
+
101
+ - **Meeting Transcription**: Record meetings for later transcription
102
+ - **Quality Assurance**: Monitor and review voice communications
103
+ - **AI Training**: Collect voice data for machine learning
104
+ - **Content Creation**: Capture voice for podcasts or videos
105
+
106
+ ## Related Examples
107
+
108
+ - [connection-test](../connection-test/) - Basic connection and event handling
109
+ - [sending-audio](../sending-audio/) - Send audio to the room
@@ -1,148 +1,256 @@
1
- const accessKey = "__YOUR_ACCESS_KEY__";
2
- const roomName = "Lobby";
3
- const userName = "My Bot";
1
+ /**
2
+ * ODIN Node.js SDK - Audio Recording Bot Example
3
+ *
4
+ * This example demonstrates how to:
5
+ * - Connect to an ODIN room as a recording bot
6
+ * - Receive audio data from other peers
7
+ * - Save audio recordings as WAV files
8
+ * - Handle peer join/leave events
9
+ *
10
+ * The bot automatically starts recording when a peer begins speaking
11
+ * and saves the audio to a WAV file when they stop.
12
+ *
13
+ * Prerequisites:
14
+ * - Replace __YOUR_ACCESS_KEY__ with your ODIN access key
15
+ * - Get a free access key at https://docs.4players.io/voice/introduction/access-keys/
16
+ */
4
17
 
5
- // Load the odin module
6
18
  import odin from '../../index.cjs';
7
- const {OdinClient} = odin;
8
-
9
- // Import wav module and OpenAI API
19
+ const { OdinClient, OdinCipher } = odin;
10
20
  import wav from 'wav';
11
- import { Configuration, OpenAIApi } from "openai";
12
- import fs from 'fs';
13
21
 
14
- // Configure OpenAI - use your own API key
15
- const configuration = new Configuration({
16
- apiKey: '__YOUR_OPENAI_API_KEY__'
17
- });
18
- const openai = new OpenAIApi(configuration);
22
+ // ============================================
23
+ // Configuration - Replace with your values
24
+ // ============================================
25
+
26
+ /**
27
+ * Your ODIN access key. Get one for free at https://docs.4players.io/voice/introduction/access-keys/
28
+ * @type {string}
29
+ */
30
+ const accessKey = "__YOUR_ACCESS_KEY__";
19
31
 
20
- // Create an odin client instance using our access key and create a room
21
- const odinClient = new OdinClient();
22
- const room = odinClient.createRoom(accessKey, roomName, userName);
32
+ /**
33
+ * The room ID to join. All users joining the same room can communicate.
34
+ * @type {string}
35
+ */
36
+ const roomId = "__YOUR_ROOM_ID__";
23
37
 
24
- // Listen on PeerJoined messages and print the user data of the joined peer
25
- room.addEventListener('PeerJoined', (event) => {
26
- console.log("Received PeerJoined event", event);
27
- console.log(JSON.parse(new TextDecoder().decode(event.userData)));
28
- });
38
+ /**
39
+ * Unique user ID for this recording bot.
40
+ * @type {string}
41
+ */
42
+ const userId = "RecorderBot-" + Math.floor(Math.random() * 10000);
29
43
 
30
- // Listen on PeerLeft messages and print the user data of the left peer
31
- room.addEventListener('PeerLeft', (event) => {
32
- console.log("Received PeerLeft event", event);
33
- });
44
+ /**
45
+ * Optional: Password for End-to-End Encryption.
46
+ * Set to null to disable, or set a string that matches other peers.
47
+ * @type {string|null}
48
+ */
49
+ const cipherPassword = null;
50
+
51
+ // ============================================
52
+ // Recording State
53
+ // ============================================
54
+
55
+ /**
56
+ * Maps media IDs to their active recording writers.
57
+ * @type {Object.<number, {wavEncoder: wav.FileWriter, fileName: string, peerId: number, sampleCount: number}>}
58
+ */
59
+ const fileRecorder = {};
60
+
61
+ /**
62
+ * Maps peer IDs to their info (name, etc.).
63
+ * @type {Map<number, {name: string}>}
64
+ */
65
+ const activePeers = new Map();
66
+
67
+ // ============================================
68
+ // Main Recording Bot
69
+ // ============================================
70
+
71
+ async function run() {
72
+ console.log("=== ODIN Audio Recording Bot ===\n");
73
+
74
+ // Step 1: Create ODIN client
75
+ console.log("1. Creating ODIN client...");
76
+ const client = new OdinClient();
77
+
78
+ // Step 2: Generate token from access key
79
+ console.log("2. Generating token from access key...");
80
+ const token = client.generateToken(accessKey, roomId, userId);
81
+ console.log(" Token generated successfully.\n");
82
+
83
+ // Step 3: Create room using factory pattern (recommended approach)
84
+ console.log("3. Creating room...");
85
+ const room = client.createRoom(token);
86
+
87
+ // Step 4: Configure E2EE if password is provided
88
+ if (cipherPassword) {
89
+ console.log("4. Configuring E2EE cipher...");
90
+ const cipher = new OdinCipher();
91
+ cipher.setPassword(new TextEncoder().encode(cipherPassword));
92
+ room.setCipher(cipher);
93
+ console.log(" E2EE enabled.\n");
94
+ } else {
95
+ console.log("4. E2EE disabled (no password configured).\n");
96
+ }
97
+
98
+ // ========================================
99
+ // Event Handlers
100
+ // ========================================
101
+
102
+ // Connection state changes
103
+ room.onConnectionStateChanged((event) => {
104
+ console.log(`[Connection] State: ${event.state}`);
105
+ });
106
+
107
+ // Room joined - we're connected
108
+ room.onJoined((event) => {
109
+ console.log(`[Room] Joined! Room ID: ${event.roomId}, Own Peer ID: ${event.ownPeerId}`);
110
+ console.log(`[Room] Available media IDs: ${JSON.stringify(event.mediaIds)}`);
111
+ });
112
+
113
+ // Room left - we're disconnected
114
+ room.onLeft((event) => {
115
+ console.log(`[Room] Left. Reason: ${event.reason}`);
116
+ });
117
+
118
+ // Peer joined - track new peers for better file naming
119
+ room.onPeerJoined((event) => {
120
+ const peerId = event.peerId;
121
+ let userName = "Unknown";
122
+
123
+ // Try to parse user data to get the peer's name
124
+ if (event.userData) {
125
+ try {
126
+ const userData = JSON.parse(new TextDecoder().decode(new Uint8Array(event.userData)));
127
+ userName = userData.name || userData.userId || "Unknown";
128
+ } catch (e) {
129
+ // Ignore parse errors - user data might be binary
130
+ }
131
+ }
132
+
133
+ activePeers.set(peerId, { name: userName });
134
+ console.log(`[Peer] Joined: ${userName} (ID: ${peerId})`);
135
+ });
136
+
137
+ // Peer left - cleanup tracking
138
+ room.onPeerLeft((event) => {
139
+ const peerInfo = activePeers.get(event.peerId);
140
+ console.log(`[Peer] Left: ${peerInfo?.name || 'Unknown'} (ID: ${event.peerId})`);
141
+ activePeers.delete(event.peerId);
142
+ });
143
+
144
+ // Media started - a peer started their audio stream
145
+ room.onMediaStarted((event) => {
146
+ console.log(`[Media] Started: Peer ${event.peerId}, Media ID: ${event.media?.id}`);
147
+ });
34
148
 
35
- // Listen on MediaActivity messages and prepare a wav file for each media stream. The basic idea here is to
36
- // create a WAV encoder file whenever a users starts talking and to close the file when the user stops talking. This way,
37
- // we have isolated WAV files for each user and can transcribe them individually. If we don't want to create new files
38
- // during short pauses, we wait 2 seconds before closing the file.
39
- room.addEventListener('MediaActivity', (event) => {
40
- if (event.state) {
41
- // User started talking - prepare a new file
42
- if (!fileRecorder[event.mediaId]) {
43
- const timer = new Date().getTime();
44
- const fileName = `./recording_${event.peerId}_${event.mediaId}_${timer}.wav`;
45
- console.log("Created a new recording file: ", fileName);
46
- fileRecorder[event.mediaId] = {
149
+ // Media stopped - finalize recording for this media stream
150
+ room.onMediaStopped((event) => {
151
+ console.log(`[Media] Stopped: Peer ${event.peerId}, Media ID: ${event.mediaId}`);
152
+
153
+ // Stop and save the recording if one exists
154
+ const recorder = fileRecorder[event.mediaId];
155
+ if (recorder) {
156
+ recorder.wavEncoder.end();
157
+ console.log(`[Recording] Saved: ${recorder.fileName} (${recorder.sampleCount} samples)`);
158
+ delete fileRecorder[event.mediaId];
159
+ }
160
+ });
161
+
162
+ // Message received - log any messages from peers
163
+ room.onMessageReceived((event) => {
164
+ console.log(`[Message] From Peer ${event.senderPeerId}`);
165
+ try {
166
+ const message = JSON.parse(new TextDecoder().decode(new Uint8Array(event.message)));
167
+ console.log(` Content:`, message);
168
+ } catch (e) {
169
+ console.log(` (Binary message, ${event.message?.length || 0} bytes)`);
170
+ }
171
+ });
172
+
173
+ // Audio data received - this is where we record!
174
+ // This event fires for each audio frame received from peers
175
+ room.onAudioDataReceived((data) => {
176
+ const mediaId = data.mediaId;
177
+
178
+ // Create a new recording file if this is a new media stream
179
+ if (!fileRecorder[mediaId]) {
180
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
181
+ const peerName = activePeers.get(data.peerId)?.name || `peer${data.peerId}`;
182
+ const fileName = `./recording_${peerName}_media${mediaId}_${timestamp}.wav`;
183
+
184
+ console.log(`[Recording] Started: ${fileName}`);
185
+
186
+ fileRecorder[mediaId] = {
47
187
  wavEncoder: new wav.FileWriter(fileName, {
48
- channels: 1,
49
- sampleRate: 48000,
50
- bitDepth: 16
188
+ channels: 2, // ODIN decoder outputs stereo (interleaved L/R)
189
+ sampleRate: 48000, // ODIN uses 48kHz
190
+ bitDepth: 16 // 16-bit samples
51
191
  }),
52
- fileName: fileName
192
+ fileName: fileName,
193
+ peerId: data.peerId,
194
+ sampleCount: 0
53
195
  };
54
- } else {
55
- // We already have a file for this media stream - reset the timer to avoid closing the file
56
- if (fileRecorder[event.mediaId].timer) {
57
- clearTimeout(fileRecorder[event.mediaId].timer);
58
- delete fileRecorder[event.mediaId].timer;
59
- }
60
196
  }
61
- } else {
62
- // User stopped talking
63
- if (fileRecorder[event.mediaId]) {
64
- // If we don't have a timer yet, create one
65
- if (!fileRecorder[event.mediaId].timer) {
66
- fileRecorder[event.mediaId].timer = setTimeout(() => {
67
- // The timer timed out - i.e. the user did stop talking for 2 seconds - close the file
68
- fileRecorder[event.mediaId].wavEncoder.end();
69
-
70
- // Transcribe the file using OpenAI
71
- try {
72
- const file = fs.createReadStream(fileRecorder[event.mediaId].fileName);
73
- openai.createTranscription(file, "whisper-1").then((response) => {
74
- console.log("OpenAI Transcription: ", response.data.text);
75
- });
76
- } catch (e) {
77
- console.log("Failed to transcribe: ", e);
78
- }
79
-
80
- // Delete the file recorder object
81
- delete fileRecorder[event.mediaId];
82
- }, 2000);
83
- }
197
+
198
+ // Write audio samples to the recording
199
+ const recorder = fileRecorder[mediaId];
200
+ if (recorder && data.samples16) {
201
+ // Convert Int16Array to Buffer and write to WAV file
202
+ const buffer = Buffer.from(data.samples16.buffer, data.samples16.byteOffset, data.samples16.byteLength);
203
+ recorder.wavEncoder.write(buffer);
204
+ recorder.sampleCount += data.samples16.length;
84
205
  }
85
- }
86
- });
206
+ });
87
207
 
88
- // Configure user data used by the bot - this user data will be compatible with ODIN Web Client (https://odin.4players.de/app).
89
- const userData = {
90
- name: "Recorder Bot",
91
- seed: "123",
92
- userId: "Bot007",
93
- outputMuted: 1,
94
- platform: "ODIN JS Bot SDK",
95
- version: "0.1"
96
- }
97
- // Create a byte array from the user data (ODIN uses byte arrays for user data for maximum flexibility)
98
- const data = new TextEncoder().encode(JSON.stringify(userData));
99
-
100
- // Join the room using the default gateway and our user data
101
- room.join("gateway.odin.4players.io", data);
102
-
103
- // Print the room-id
104
- console.log("ROOM-ID:", room.id);
105
-
106
- // Add an event filter for audio data received events
107
- room.addEventListener('AudioDataReceived', (data) => {
108
- // Getting an array of the sample buffer - use for example to visualize audio
109
- /*
110
- let ui32 = new Float32Array(data.samples32.buffer);
111
- console.log(ui32);
112
-
113
- let ui16 = new Int16Array(data.samples16.buffer);
114
- console.log(ui16);
115
- */
116
-
117
- // Write the audio data to the file using a WAV encoder
118
- if (fileRecorder[data.mediaId]) {
119
- fileRecorder[data.mediaId].wavEncoder.file.write(data.samples16, (error) => {
120
- if (error) {
121
- console.log("Failed to write audio file");
122
- }
123
- });
124
- }
125
- });
208
+ // ========================================
209
+ // Join the room
210
+ // ========================================
126
211
 
127
- // Prepare a message compatible with the ODIN Web Client and send it to all users (see @4players/odin-foundation for more info)
128
- const message = {
129
- kind: 'message',
130
- payload: {
131
- text: 'Hello World'
212
+ console.log("5. Joining room...");
213
+ try {
214
+ room.join("https://gateway.odin.4players.io");
215
+ console.log(" Join initiated.\n");
216
+ } catch (e) {
217
+ console.error("Failed to join:", e.message);
218
+ process.exit(1);
132
219
  }
220
+
221
+ // ========================================
222
+ // Graceful shutdown handler
223
+ // ========================================
224
+
225
+ console.log("Recording bot is running. Press Ctrl+C to stop.\n");
226
+
227
+ const shutdown = () => {
228
+ console.log("\n[Shutdown] Stopping...");
229
+
230
+ // Close all active recordings
231
+ for (const [mediaId, recorder] of Object.entries(fileRecorder)) {
232
+ console.log(`[Recording] Finalizing: ${recorder.fileName} (${recorder.sampleCount} samples)`);
233
+ recorder.wavEncoder.end();
234
+ }
235
+
236
+ // Close room connection
237
+ room.close();
238
+ console.log("[Shutdown] Room closed.");
239
+ process.exit(0);
240
+ };
241
+
242
+ process.on('SIGINT', shutdown);
243
+ process.on('SIGTERM', shutdown);
244
+
245
+ // Keep the process alive indefinitely
246
+ await new Promise(() => { }); // Never resolves - run until interrupted
133
247
  }
134
- room.sendMessage(new TextEncoder().encode(JSON.stringify(message)));
135
-
136
- // Wait for a key press to stop the script
137
- console.log("Press any key to stop");
138
- const stdin = process.stdin;
139
- stdin.resume();
140
- stdin.setEncoding( 'utf8' );
141
- stdin.on( 'data', function( key )
142
- {
143
- console.log("Shutting down");
144
- room.close();
145
- fileWriter.end();
146
-
147
- process.exit();
248
+
249
+ // ============================================
250
+ // Entry Point
251
+ // ============================================
252
+
253
+ run().catch(err => {
254
+ console.error("Fatal error:", err);
255
+ process.exit(1);
148
256
  });