@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
|
@@ -1,20 +1,189 @@
|
|
|
1
|
-
# ODIN
|
|
1
|
+
# ODIN Audio Sending Examples
|
|
2
2
|
|
|
3
|
-
This
|
|
4
|
-
|
|
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
|
-
|
|
9
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
20
|
-
|
|
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
|
|
Binary file
|
|
@@ -1,107 +1,270 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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
|
|
10
|
-
import
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
//
|
|
27
|
-
const
|
|
28
|
-
const
|
|
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
|
-
//
|
|
31
|
-
//
|
|
39
|
+
// ============================================
|
|
40
|
+
// Configuration - Replace with your values
|
|
41
|
+
// ============================================
|
|
32
42
|
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
58
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Your unique user identifier.
|
|
57
|
+
* @type {string}
|
|
58
|
+
*/
|
|
59
|
+
const userId = "AudioBot-" + Math.floor(Math.random() * 10000);
|
|
59
60
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
//
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
83
|
-
|
|
136
|
+
// ============================================
|
|
137
|
+
// Main Function
|
|
138
|
+
// ============================================
|
|
84
139
|
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
//
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
//
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
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
|
+
});
|