@4players/odin-nodejs 0.10.3 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +72 -0
- package/LICENSE +21 -0
- package/README.md +603 -44
- package/binding.gyp +29 -13
- package/cppsrc/binding.cpp +3 -6
- package/cppsrc/odinbindings.cpp +9 -45
- package/cppsrc/odincipher.cpp +92 -0
- package/cppsrc/odincipher.h +32 -0
- package/cppsrc/odinclient.cpp +19 -158
- package/cppsrc/odinclient.h +2 -5
- package/cppsrc/odinmedia.cpp +144 -186
- package/cppsrc/odinmedia.h +51 -18
- package/cppsrc/odinroom.cpp +675 -635
- package/cppsrc/odinroom.h +76 -26
- package/cppsrc/utilities.cpp +11 -81
- package/cppsrc/utilities.h +25 -140
- package/index.cjs +829 -0
- package/index.d.ts +3 -4
- package/libs/bin/linux/arm64/libodin.so +0 -0
- package/libs/bin/linux/arm64/libodin_crypto.so +0 -0
- package/libs/bin/linux/ia32/libodin.so +0 -0
- package/libs/bin/linux/ia32/libodin_crypto.so +0 -0
- package/libs/bin/linux/x64/libodin.so +0 -0
- package/libs/bin/linux/x64/libodin_crypto.so +0 -0
- package/{prebuilds/darwin-x64/node.napi.node → libs/bin/macos/universal/libodin.dylib} +0 -0
- package/libs/bin/macos/universal/libodin_crypto.dylib +0 -0
- package/libs/bin/windows/arm64/odin.dll +0 -0
- package/libs/bin/windows/arm64/odin.lib +0 -0
- package/libs/bin/windows/arm64/odin_crypto.dll +0 -0
- package/libs/bin/windows/arm64/odin_crypto.lib +0 -0
- package/libs/bin/windows/ia32/odin.dll +0 -0
- package/libs/bin/windows/ia32/odin.lib +0 -0
- package/libs/bin/windows/ia32/odin_crypto.dll +0 -0
- package/libs/bin/windows/ia32/odin_crypto.lib +0 -0
- package/libs/bin/windows/x64/odin.dll +0 -0
- package/libs/bin/windows/x64/odin.lib +0 -0
- package/libs/bin/windows/x64/odin_crypto.dll +0 -0
- package/libs/bin/windows/x64/odin_crypto.lib +0 -0
- package/libs/include/odin.h +665 -567
- package/libs/include/odin_crypto.h +46 -0
- package/odin.cipher.d.ts +31 -0
- package/odin.media.d.ts +69 -19
- package/odin.room.d.ts +348 -7
- package/package.json +5 -4
- package/prebuilds/{darwin-arm64/node.napi.node → darwin-x64+arm64/libodin.dylib} +0 -0
- package/prebuilds/darwin-x64+arm64/libodin_crypto.dylib +0 -0
- package/prebuilds/darwin-x64+arm64/node.napi.node +0 -0
- package/prebuilds/linux-x64/libodin.so +0 -0
- package/prebuilds/linux-x64/libodin_crypto.so +0 -0
- package/prebuilds/linux-x64/node.napi.node +0 -0
- package/prebuilds/win32-x64/node.napi.node +0 -0
- package/prebuilds/win32-x64/odin.dll +0 -0
- package/prebuilds/win32-x64/odin_crypto.dll +0 -0
- package/scripts/postbuild.cjs +133 -0
- package/tests/audio-recording/README.md +97 -12
- package/tests/audio-recording/index.js +238 -130
- package/tests/connection-test/README.md +97 -0
- package/tests/connection-test/index.js +273 -0
- package/tests/lifecycle/test-room-cycle.js +169 -0
- package/tests/sending-audio/README.md +178 -9
- package/tests/sending-audio/canBounce.mp3 +0 -0
- package/tests/sending-audio/index.js +250 -87
- package/tests/sending-audio/test-kiss-api.js +149 -0
- package/tests/sending-audio/test-loop-audio.js +142 -0
- package/CMakeLists.txt +0 -25
- package/libs/bin/linux/arm64/libodin_static.a +0 -0
- package/libs/bin/linux/ia32/libodin_static.a +0 -0
- package/libs/bin/linux/x64/libodin_static.a +0 -0
- package/libs/bin/macos/arm64/libodin_static.a +0 -0
- package/libs/bin/macos/x64/libodin_static.a +0 -0
- package/libs/bin/windows/arm64/odin_static.lib +0 -0
- package/libs/bin/windows/ia32/odin_static.lib +0 -0
- package/libs/bin/windows/x64/odin_static.lib +0 -0
|
@@ -0,0 +1,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
|
|
1
|
+
# ODIN Audio Recording Bot Example
|
|
2
2
|
|
|
3
|
-
This
|
|
4
|
-
|
|
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
|
-
|
|
9
|
-
|
|
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
|
-
|
|
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
|
-
|
|
30
|
+
```javascript
|
|
31
|
+
const cipherPassword = "shared-secret";
|
|
32
|
+
```
|
|
14
33
|
|
|
15
|
-
|
|
34
|
+
## Running the Bot
|
|
16
35
|
|
|
17
36
|
```bash
|
|
37
|
+
cd tests/audio-recording
|
|
18
38
|
node index.js
|
|
19
39
|
```
|
|
20
40
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
//
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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:
|
|
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
|
-
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
});
|