@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/cppsrc/odinmedia.cpp
CHANGED
|
@@ -1,246 +1,204 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Created by Phillip Schuster on 11.02.23.
|
|
3
|
-
//
|
|
4
|
-
|
|
5
1
|
#include "odinmedia.h"
|
|
6
|
-
#include
|
|
2
|
+
#include "odinroom.h"
|
|
3
|
+
#include "utilities.h"
|
|
7
4
|
#include <iostream>
|
|
8
|
-
#include <codecvt>
|
|
9
|
-
#include <locale>
|
|
10
5
|
|
|
11
6
|
using namespace std;
|
|
12
7
|
|
|
13
|
-
|
|
14
|
-
Napi::FunctionReference *OdinMedia::constructor;
|
|
8
|
+
Napi::FunctionReference *OdinMediaWrapper::constructor;
|
|
15
9
|
|
|
16
10
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
11
|
+
* Constructs an OdinMediaWrapper for encoding and transmitting audio.
|
|
12
|
+
* This maps to odin_encoder_create() in the core SDK.
|
|
13
|
+
*
|
|
14
|
+
* @param info[0] OdinRoom instance - parent room for transmission
|
|
15
|
+
* @param info[1] Sample rate (number) - typically 48000
|
|
16
|
+
* @param info[2] Number of channels (number) - 1 for mono, 2 for stereo
|
|
19
17
|
*/
|
|
20
|
-
|
|
18
|
+
OdinMediaWrapper::OdinMediaWrapper(const Napi::CallbackInfo &info) : Napi::ObjectWrap<OdinMediaWrapper>(info) {
|
|
21
19
|
Napi::Env env = info.Env();
|
|
22
|
-
Napi::HandleScope scope(env);
|
|
23
|
-
|
|
24
|
-
uint16_t out_id;
|
|
25
|
-
OdinReturnCode error = odin_media_stream_media_id(_mediaStreamHandle, &out_id);
|
|
26
|
-
if (odin_is_error(error)) {
|
|
27
|
-
Napi::TypeError::New(env, "Failed to get media id").ThrowAsJavaScriptException();
|
|
28
|
-
return env.Undefined();
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
return Napi::Number::New(env, out_id);
|
|
32
|
-
}
|
|
33
20
|
|
|
34
|
-
/**
|
|
35
|
-
* Creates a new OdinMedia instance. Requires an Odin token as a parameter.
|
|
36
|
-
* @param info Napi::CallbackInfo
|
|
37
|
-
*/
|
|
38
|
-
OdinMedia::OdinMedia(const Napi::CallbackInfo &info) : Napi::ObjectWrap<OdinMedia>(info) {
|
|
39
|
-
Napi::Env env = info.Env();
|
|
40
|
-
Napi::HandleScope scope(env);
|
|
41
|
-
|
|
42
|
-
// Checking for room instance, sample rate (float) and channels (int)
|
|
43
21
|
if (info.Length() < 3 || !info[0].IsObject() || !info[1].IsNumber() || !info[2].IsNumber()) {
|
|
44
|
-
Napi::TypeError::New(env, "Provide an OdinRoom instance, sample rate and number of channels
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (info.Length() == 4 && !info[3].IsObject()) {
|
|
49
|
-
Napi::TypeError::New(env, "Options need to be provided as an object").ThrowAsJavaScriptException();
|
|
22
|
+
Napi::TypeError::New(env, "Provide an OdinRoom instance, sample rate and number of channels").ThrowAsJavaScriptException();
|
|
50
23
|
return;
|
|
51
24
|
}
|
|
52
25
|
|
|
53
|
-
|
|
26
|
+
_roomWrapper = Napi::ObjectWrap<OdinRoomWrapper>::Unwrap(info[0].As<Napi::Object>());
|
|
54
27
|
_sampleRate = info[1].As<Napi::Number>().Uint32Value();
|
|
55
28
|
_numChannels = info[2].As<Napi::Number>().Int32Value();
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
apm_config.volume_gate = false;
|
|
65
|
-
apm_config.volume_gate_attack_loudness = -30;
|
|
66
|
-
apm_config.volume_gate_release_loudness = -40;
|
|
67
|
-
apm_config.echo_canceller = false;
|
|
68
|
-
apm_config.high_pass_filter = false;
|
|
69
|
-
apm_config.pre_amplifier = false;
|
|
70
|
-
apm_config.noise_suppression_level = OdinNoiseSuppressionLevel_Moderate;
|
|
71
|
-
apm_config.transient_suppressor = false;
|
|
72
|
-
apm_config.gain_controller = true;
|
|
73
|
-
|
|
74
|
-
if (info.Length() == 4) {
|
|
75
|
-
Napi::Object options = info[3].As<Napi::Object>();
|
|
76
|
-
if (options.Has("voiceActivityDetection")) {
|
|
77
|
-
apm_config.voice_activity_detection = options.Get("voiceActivityDetection").As<Napi::Boolean>().Value();
|
|
78
|
-
}
|
|
79
|
-
if (options.Has("voiceActivityDetectionAttackProbability")) {
|
|
80
|
-
apm_config.voice_activity_detection_attack_probability = options.Get("voiceActivityDetectionAttackProbability").As<Napi::Number>().DoubleValue();
|
|
81
|
-
}
|
|
82
|
-
if (options.Has("voiceActivityDetectionReleaseProbability")) {
|
|
83
|
-
apm_config.voice_activity_detection_release_probability = options.Get("voiceActivityDetectionReleaseProbability").As<Napi::Number>().DoubleValue();
|
|
84
|
-
}
|
|
85
|
-
if (options.Has("volumeGate")) {
|
|
86
|
-
apm_config.volume_gate = options.Get("volumeGate").As<Napi::Boolean>().Value();
|
|
87
|
-
}
|
|
88
|
-
if (options.Has("volumeGateAttackLoudness")) {
|
|
89
|
-
apm_config.volume_gate_attack_loudness = options.Get("volumeGateAttackLoudness").As<Napi::Number>().DoubleValue();
|
|
90
|
-
}
|
|
91
|
-
if (options.Has("volumeGateReleaseLoudness")) {
|
|
92
|
-
apm_config.volume_gate_release_loudness = options.Get("volumeGateReleaseLoudness").As<Napi::Number>().DoubleValue();
|
|
93
|
-
}
|
|
94
|
-
if (options.Has("echoCanceller")) {
|
|
95
|
-
apm_config.echo_canceller = options.Get("echoCanceller").As<Napi::Boolean>().Value();
|
|
96
|
-
}
|
|
97
|
-
if (options.Has("highPassFilter")) {
|
|
98
|
-
apm_config.high_pass_filter = options.Get("highPassFilter").As<Napi::Boolean>().Value();
|
|
99
|
-
}
|
|
100
|
-
if (options.Has("preAmplifier")) {
|
|
101
|
-
apm_config.pre_amplifier = options.Get("preAmplifier").As<Napi::Boolean>().Value();
|
|
102
|
-
}
|
|
103
|
-
if (options.Has("noiseSuppressionLevel")) {
|
|
104
|
-
apm_config.noise_suppression_level = (OdinNoiseSuppressionLevel) options.Get("noiseSuppressionLevel").As<Napi::Number>().Int32Value();
|
|
105
|
-
}
|
|
106
|
-
if (options.Has("transientSuppressor")) {
|
|
107
|
-
apm_config.transient_suppressor = options.Get("transientSuppressor").As<Napi::Boolean>().Value();
|
|
108
|
-
}
|
|
109
|
-
if (options.Has("gainController")) {
|
|
110
|
-
apm_config.gain_controller = options.Get("gainController").As<Napi::Boolean>().Value();
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
OdinReturnCode error = odin_room_configure_apm(_room->GetRoomHandle(), apm_config);
|
|
115
|
-
if (odin_is_error(error)) {
|
|
116
|
-
OdinUtilities::ThrowNapiException(env, error, "Failed to configure APM settings");
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/*
|
|
120
|
-
* Create the input audio stream with a samplerate of 48 kHz
|
|
121
|
-
*/
|
|
122
|
-
OdinAudioStreamConfig audio_config;
|
|
123
|
-
audio_config.sample_rate = _sampleRate;
|
|
124
|
-
audio_config.channel_count = _numChannels;
|
|
125
|
-
_mediaStreamHandle = odin_audio_stream_create(audio_config);
|
|
126
|
-
|
|
127
|
-
error = odin_room_add_media(_room->GetRoomHandle(), _mediaStreamHandle);
|
|
128
|
-
if (odin_is_error(error)) {
|
|
129
|
-
OdinUtilities::ThrowNapiException(env, error, "Failed to add media to room");
|
|
29
|
+
_mediaId = 0; // Will be set via setMediaId() after Joined event
|
|
30
|
+
_closed = false;
|
|
31
|
+
_encoder = nullptr;
|
|
32
|
+
|
|
33
|
+
// Create the ODIN encoder with the specified sample rate and stereo mode
|
|
34
|
+
OdinError rc = odin_encoder_create(_sampleRate, _numChannels == 2, &_encoder);
|
|
35
|
+
if (odin_is_error(rc)) {
|
|
36
|
+
OdinUtilities::ThrowNapiException(env, rc, "Failed to create encoder");
|
|
130
37
|
}
|
|
131
38
|
}
|
|
132
39
|
|
|
133
40
|
/**
|
|
134
|
-
*
|
|
41
|
+
* Releases resources when the JavaScript object is garbage collected.
|
|
42
|
+
* Uses mutex to prevent race conditions with Close().
|
|
135
43
|
*/
|
|
136
|
-
void
|
|
137
|
-
|
|
44
|
+
void OdinMediaWrapper::Finalize(Napi::Env env) {
|
|
45
|
+
std::lock_guard<std::mutex> lock(_closeMutex);
|
|
46
|
+
|
|
47
|
+
_closed = true;
|
|
48
|
+
|
|
49
|
+
if (_encoder) {
|
|
50
|
+
odin_encoder_free(_encoder);
|
|
51
|
+
_encoder = nullptr;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
_roomWrapper = nullptr;
|
|
138
55
|
}
|
|
139
56
|
|
|
140
57
|
/**
|
|
141
|
-
*
|
|
142
|
-
*
|
|
58
|
+
* Initializes the OdinMedia class and registers it with the N-API module.
|
|
59
|
+
* Exposes: sendAudioData, setMediaId, close, and id property.
|
|
60
|
+
*
|
|
61
|
+
* Note: start() and stop() were removed as they served no purpose -
|
|
62
|
+
* they set a _running flag that was never checked. This simplifies
|
|
63
|
+
* the API to match the core ODIN SDK pattern.
|
|
143
64
|
*/
|
|
144
|
-
Napi::Object
|
|
145
|
-
// This method is used to hook the accessor and method callbacks
|
|
65
|
+
Napi::Object OdinMediaWrapper::Init(Napi::Env env, Napi::Object exports) {
|
|
146
66
|
Napi::Function func = DefineClass(env, "OdinMedia", {
|
|
147
|
-
InstanceMethod<&
|
|
148
|
-
InstanceMethod<&
|
|
149
|
-
|
|
150
|
-
|
|
67
|
+
InstanceMethod<&OdinMediaWrapper::Close>("close", static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
|
|
68
|
+
InstanceMethod<&OdinMediaWrapper::SendData>("sendAudioData", static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
|
|
69
|
+
InstanceMethod<&OdinMediaWrapper::SetMediaId>("setMediaId", static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
|
|
70
|
+
InstanceAccessor("id", &OdinMediaWrapper::MediaId, nullptr, static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
|
|
71
|
+
StaticMethod<&OdinMediaWrapper::CreateNewItem>("CreateNewItem", static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
|
|
151
72
|
});
|
|
152
73
|
|
|
153
|
-
// We use a static variable to store the constructor. This is because we need to create instances within C++ and
|
|
154
|
-
// expose to JavaScript. We can't use the constructor directly because it's not exposed to JavaScript.
|
|
155
74
|
constructor = new Napi::FunctionReference();
|
|
156
|
-
|
|
157
|
-
// Create a persistent reference to the class constructor. This will allow
|
|
158
|
-
// a function called on a class prototype and a function
|
|
159
|
-
// called on instance of a class to be distinguished from each other.
|
|
160
75
|
*constructor = Napi::Persistent(func);
|
|
161
76
|
exports.Set("OdinMedia", func);
|
|
162
77
|
|
|
163
|
-
// Store the constructor as the add-on instance data. This will allow this
|
|
164
|
-
// add-on to support multiple instances of itself running on multiple worker
|
|
165
|
-
// threads, as well as multiple instances of itself running in different
|
|
166
|
-
// contexts on the same thread.
|
|
167
|
-
//
|
|
168
|
-
// By default, the value set on the environment here will be destroyed when
|
|
169
|
-
// the add-on is unloaded using the `delete` operator, but it is also
|
|
170
|
-
// possible to supply a custom deleter.
|
|
171
|
-
env.SetInstanceData<Napi::FunctionReference>(constructor);
|
|
172
|
-
|
|
173
78
|
return exports;
|
|
174
79
|
}
|
|
175
80
|
|
|
176
81
|
/**
|
|
177
|
-
* Closes the
|
|
178
|
-
*
|
|
82
|
+
* Closes the media stream and releases resources.
|
|
83
|
+
* This maps to odin_encoder_free() in the core SDK.
|
|
84
|
+
*
|
|
85
|
+
* Thread Safety:
|
|
86
|
+
* Sets _closed to true FIRST (atomically), then acquires the mutex to free resources.
|
|
87
|
+
* This ensures that other methods see _closed=true immediately and exit early.
|
|
179
88
|
*/
|
|
180
|
-
void
|
|
181
|
-
//
|
|
89
|
+
void OdinMediaWrapper::Close(const Napi::CallbackInfo &info) {
|
|
90
|
+
// Set closed flag first - this is atomic and immediately visible to other methods.
|
|
91
|
+
bool wasClosed = _closed.exchange(true);
|
|
92
|
+
if (wasClosed) {
|
|
93
|
+
return; // Already closed
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Acquire mutex to safely free resources
|
|
97
|
+
std::lock_guard<std::mutex> lock(_closeMutex);
|
|
98
|
+
|
|
99
|
+
if (_encoder) {
|
|
100
|
+
odin_encoder_free(_encoder);
|
|
101
|
+
_encoder = nullptr;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
_roomWrapper = nullptr;
|
|
105
|
+
}
|
|
182
106
|
|
|
183
|
-
|
|
107
|
+
Napi::Value OdinMediaWrapper::CreateNewItem(const Napi::CallbackInfo &info) {
|
|
108
|
+
return constructor->New({info[0]});
|
|
109
|
+
}
|
|
184
110
|
|
|
185
|
-
|
|
186
|
-
|
|
111
|
+
Napi::Object OdinMediaWrapper::NewInstance(const std::vector<napi_value>& args) {
|
|
112
|
+
return constructor->New(args);
|
|
187
113
|
}
|
|
188
114
|
|
|
189
115
|
/**
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
116
|
+
* Sets the server-assigned media ID.
|
|
117
|
+
* This must be called after receiving the Joined event which provides
|
|
118
|
+
* the list of available media IDs for this peer.
|
|
119
|
+
*
|
|
120
|
+
* @param info[0] Media ID (number) - from Joined event's mediaIds array
|
|
193
121
|
*/
|
|
194
|
-
|
|
122
|
+
void OdinMediaWrapper::SetMediaId(const Napi::CallbackInfo &info) {
|
|
123
|
+
if (_closed) return;
|
|
124
|
+
|
|
195
125
|
Napi::Env env = info.Env();
|
|
196
|
-
|
|
197
|
-
if (info.Length() < 1 || !info[0].
|
|
198
|
-
Napi::TypeError::New(env, "
|
|
126
|
+
|
|
127
|
+
if (info.Length() < 1 || !info[0].IsNumber()) {
|
|
128
|
+
Napi::TypeError::New(env, "Media ID (number) required").ThrowAsJavaScriptException();
|
|
129
|
+
return;
|
|
199
130
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
printf("Odin NodeJS Addon: Creating OdinMedia Instance\n");
|
|
203
|
-
#endif
|
|
204
|
-
|
|
205
|
-
// Retrieve the instance data we stored during `Init()`. We only stored the
|
|
206
|
-
// constructor there, so we retrieve it here to create a new instance of the
|
|
207
|
-
// JS class the constructor represents.
|
|
208
|
-
Napi::FunctionReference *constructor =
|
|
209
|
-
info.Env().GetInstanceData<Napi::FunctionReference>();
|
|
210
|
-
return constructor->New({info[0]});
|
|
131
|
+
|
|
132
|
+
_mediaId = static_cast<uint16_t>(info[0].As<Napi::Number>().Uint32Value());
|
|
211
133
|
}
|
|
212
134
|
|
|
213
135
|
/**
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
136
|
+
* Sends audio samples for transmission.
|
|
137
|
+
* This maps to odin_encoder_push() + odin_encoder_pop() + odin_room_send_datagram() in the core SDK.
|
|
138
|
+
*
|
|
139
|
+
* Pushes samples to the encoder, then immediately pops and sends all available datagrams.
|
|
140
|
+
* If mediaId is 0 or the stream is closed, this is a no-op (matches core SDK behavior).
|
|
141
|
+
*
|
|
142
|
+
* @param info[0] Float32Array of interleaved audio samples in range [-1, 1]
|
|
217
143
|
*/
|
|
218
|
-
|
|
219
|
-
//
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
printf("Odin NodeJS Addon: Creating OdinMedia Instance with %d arguments\n", (int)args.size());
|
|
223
|
-
#endif
|
|
224
|
-
|
|
225
|
-
return constructor->New(args);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
void OdinMedia::SendData(const Napi::CallbackInfo &info) {
|
|
144
|
+
void OdinMediaWrapper::SendData(const Napi::CallbackInfo &info) {
|
|
145
|
+
// Early exit if closed - prevents use-after-free
|
|
146
|
+
if (_closed) return;
|
|
147
|
+
|
|
229
148
|
Napi::Env env = info.Env();
|
|
230
149
|
|
|
231
150
|
if (info.Length() < 1 || !info[0].IsTypedArray()) {
|
|
232
|
-
Napi::TypeError::New(env, "
|
|
151
|
+
Napi::TypeError::New(env, "Float32Array required as first parameter").ThrowAsJavaScriptException();
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Check resources are valid
|
|
156
|
+
if (!_encoder || !_roomWrapper || !_roomWrapper->GetRoom()) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// mediaId must be set before sending (will produce no output if 0)
|
|
161
|
+
if (_mediaId == 0) {
|
|
233
162
|
return;
|
|
234
163
|
}
|
|
235
164
|
|
|
236
165
|
Napi::Float32Array array = info[0].As<Napi::Float32Array>();
|
|
166
|
+
const float* samples = array.Data();
|
|
167
|
+
uint32_t sampleCount = array.ElementLength();
|
|
237
168
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
169
|
+
// Push samples to encoder
|
|
170
|
+
OdinError err = odin_encoder_push(_encoder, samples, sampleCount);
|
|
171
|
+
if (odin_is_error(err)) {
|
|
172
|
+
std::cerr << "odin_encoder_push failed: " << odin_error_get_last_error() << std::endl;
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
241
175
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
176
|
+
// Pop and send all available datagrams
|
|
177
|
+
uint8_t datagram[2048];
|
|
178
|
+
for (;;) {
|
|
179
|
+
if (_closed) return; // Check again in loop
|
|
180
|
+
|
|
181
|
+
uint32_t datagram_length = sizeof(datagram);
|
|
182
|
+
|
|
183
|
+
switch (odin_encoder_pop(_encoder, &_mediaId, 1, datagram, &datagram_length)) {
|
|
184
|
+
case ODIN_ERROR_SUCCESS:
|
|
185
|
+
if (_roomWrapper && _roomWrapper->GetRoom()) {
|
|
186
|
+
odin_room_send_datagram(_roomWrapper->GetRoom(), datagram, datagram_length);
|
|
187
|
+
}
|
|
188
|
+
break;
|
|
189
|
+
case ODIN_ERROR_NO_DATA:
|
|
190
|
+
return;
|
|
191
|
+
default:
|
|
192
|
+
std::cerr << "odin_encoder_pop failed: " << odin_error_get_last_error() << std::endl;
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
245
195
|
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Returns the media ID for this stream.
|
|
200
|
+
* @returns The server-assigned media ID, or 0 if not yet set.
|
|
201
|
+
*/
|
|
202
|
+
Napi::Value OdinMediaWrapper::MediaId(const Napi::CallbackInfo &info) {
|
|
203
|
+
return Napi::Number::New(info.Env(), _mediaId);
|
|
246
204
|
}
|
package/cppsrc/odinmedia.h
CHANGED
|
@@ -1,35 +1,68 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Created by Phillip Schuster on 11.02.23.
|
|
3
|
-
//
|
|
4
|
-
|
|
5
1
|
#ifndef ODIN_NODEJS_ODINMEDIA_H
|
|
6
2
|
#define ODIN_NODEJS_ODINMEDIA_H
|
|
7
3
|
|
|
8
4
|
#include <napi.h>
|
|
9
5
|
#include <odin.h>
|
|
10
|
-
#include
|
|
11
|
-
#include <
|
|
12
|
-
#include <
|
|
13
|
-
|
|
6
|
+
#include <vector>
|
|
7
|
+
#include <atomic>
|
|
8
|
+
#include <mutex>
|
|
9
|
+
|
|
10
|
+
class OdinRoomWrapper;
|
|
14
11
|
|
|
15
|
-
|
|
12
|
+
/**
|
|
13
|
+
* OdinMediaWrapper handles encoding and transmitting audio data to the ODIN server.
|
|
14
|
+
*
|
|
15
|
+
* This wrapper closely follows the core ODIN SDK pattern:
|
|
16
|
+
* 1. Constructor creates encoder (odin_encoder_create)
|
|
17
|
+
* 2. setMediaId() sets the server-assigned media ID
|
|
18
|
+
* 3. sendAudioData() encodes samples and sends datagrams (odin_encoder_push/pop + odin_room_send_datagram)
|
|
19
|
+
* 4. close() frees the encoder (odin_encoder_free)
|
|
20
|
+
*
|
|
21
|
+
* Usage workflow (from JavaScript):
|
|
22
|
+
* 1. Wait for Joined event to receive available media_ids
|
|
23
|
+
* 2. Create audio stream: media = room.createAudioStream(48000, 2)
|
|
24
|
+
* 3. Set the server-assigned media ID: media.setMediaId(mediaIds[0])
|
|
25
|
+
* 4. Push audio chunks: media.sendAudioData(chunk) // 20ms chunks recommended
|
|
26
|
+
* 5. When done: media.close()
|
|
27
|
+
*
|
|
28
|
+
* Thread Safety:
|
|
29
|
+
* The close() method sets _closed atomically before freeing resources.
|
|
30
|
+
* All methods check _closed first to prevent use-after-free crashes.
|
|
31
|
+
*/
|
|
32
|
+
class OdinMediaWrapper : public Napi::ObjectWrap<OdinMediaWrapper> {
|
|
16
33
|
public:
|
|
17
|
-
|
|
18
|
-
static Napi::FunctionReference *New;
|
|
34
|
+
OdinMediaWrapper(const Napi::CallbackInfo& info);
|
|
19
35
|
static Napi::Object Init(Napi::Env env, Napi::Object exports);
|
|
20
36
|
static Napi::Value CreateNewItem(const Napi::CallbackInfo& info);
|
|
37
|
+
static Napi::Object NewInstance(const std::vector<napi_value>& args);
|
|
21
38
|
void Finalize(Napi::Env env) override;
|
|
22
|
-
|
|
23
|
-
static Napi::Object NewInstance(const std::initializer_list<napi_value>& args);
|
|
39
|
+
|
|
24
40
|
private:
|
|
25
|
-
|
|
26
|
-
|
|
41
|
+
static Napi::FunctionReference *constructor;
|
|
42
|
+
|
|
43
|
+
// Room and encoder references
|
|
44
|
+
OdinRoomWrapper* _roomWrapper;
|
|
45
|
+
struct OdinEncoder* _encoder;
|
|
27
46
|
uint32_t _sampleRate;
|
|
28
|
-
|
|
47
|
+
int _numChannels;
|
|
48
|
+
|
|
49
|
+
// Server-assigned media ID (received from Joined event)
|
|
50
|
+
uint16_t _mediaId;
|
|
51
|
+
|
|
52
|
+
// Closed state - once set, all operations are rejected to prevent use-after-free.
|
|
53
|
+
// This is set BEFORE freeing resources in Close() to ensure thread safety.
|
|
54
|
+
std::atomic<bool> _closed;
|
|
55
|
+
|
|
56
|
+
// Mutex for protecting resource cleanup
|
|
57
|
+
std::mutex _closeMutex;
|
|
58
|
+
|
|
59
|
+
// Native methods exposed to JavaScript
|
|
29
60
|
void Close(const Napi::CallbackInfo& info);
|
|
30
61
|
void SendData(const Napi::CallbackInfo& info);
|
|
31
|
-
|
|
62
|
+
void SetMediaId(const Napi::CallbackInfo& info);
|
|
63
|
+
Napi::Value MediaId(const Napi::CallbackInfo& info);
|
|
32
64
|
};
|
|
33
65
|
|
|
34
|
-
|
|
35
66
|
#endif //ODIN_NODEJS_ODINMEDIA_H
|
|
67
|
+
|
|
68
|
+
|