@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/odinroom.cpp
CHANGED
|
@@ -1,787 +1,827 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Created by Phillip Schuster on 11.02.23.
|
|
3
|
-
//
|
|
4
|
-
|
|
5
1
|
#include "odinroom.h"
|
|
6
|
-
#include <string>
|
|
7
|
-
#include <iostream>
|
|
8
|
-
#include <codecvt>
|
|
9
|
-
#include <locale>
|
|
10
2
|
#include "odinmedia.h"
|
|
3
|
+
#include "odincipher.h"
|
|
4
|
+
#include "utilities.h"
|
|
5
|
+
#include <iostream>
|
|
6
|
+
#include <vector>
|
|
7
|
+
#include <cstring>
|
|
8
|
+
#include <cstdio>
|
|
11
9
|
|
|
12
10
|
using namespace std;
|
|
13
11
|
|
|
12
|
+
|
|
13
|
+
|
|
14
14
|
// Required, otherwise an unknown symbol error comes up
|
|
15
|
-
Napi::FunctionReference*
|
|
15
|
+
Napi::FunctionReference* OdinRoomWrapper::constructor;
|
|
16
|
+
struct OdinConnectionPool* OdinRoomWrapper::_pool = nullptr;
|
|
17
|
+
std::map<uint64_t, OdinRoomWrapper*> OdinRoomWrapper::_roomsMap;
|
|
18
|
+
std::mutex OdinRoomWrapper::_roomsMapMutex;
|
|
19
|
+
|
|
20
|
+
// Shared SDK initialization flag (also used by OdinClient)
|
|
21
|
+
bool g_odinSdkInitialized = false;
|
|
22
|
+
|
|
23
|
+
// Static counter for active rooms - used for SDK lifecycle management
|
|
24
|
+
static std::atomic<int> g_activeRoomCount{0};
|
|
25
|
+
static std::mutex g_sdkLifecycleMutex;
|
|
16
26
|
|
|
17
27
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* @param event The event data
|
|
21
|
-
* @return
|
|
28
|
+
* Increments the active room count and initializes the SDK if this is the first room.
|
|
29
|
+
* Thread-safe for concurrent room creation.
|
|
22
30
|
*/
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
obj.Set("ownUserId", joined->UserId);
|
|
33
|
-
Napi::ArrayBuffer buffer = Napi::ArrayBuffer::New(env, (void*)joined->Data, joined->DataLen);
|
|
34
|
-
Napi::Uint8Array array = Napi::Uint8Array::New(env, joined->DataLen, buffer, 0, napi_uint8_array);
|
|
35
|
-
obj.Set("roomUserData", array);
|
|
36
|
-
break;
|
|
37
|
-
}
|
|
38
|
-
case OdinEvent_PeerJoined: {
|
|
39
|
-
const PeerJoinedEventData *joined = static_cast<const PeerJoinedEventData*>(event);
|
|
40
|
-
obj.Set("peerId", joined->PeerId);
|
|
41
|
-
obj.Set("userId", joined->UserId);
|
|
42
|
-
Napi::ArrayBuffer buffer = Napi::ArrayBuffer::New(env, (void*)joined->Data, joined->DataLen);
|
|
43
|
-
Napi::Uint8Array array = Napi::Uint8Array::New(env, joined->DataLen, buffer, 0, napi_uint8_array);
|
|
44
|
-
obj.Set("userData", array);
|
|
45
|
-
break;
|
|
46
|
-
}
|
|
47
|
-
case OdinEvent_PeerLeft: {
|
|
48
|
-
const PeerLeftEventData *left = static_cast<const PeerLeftEventData*>(event);
|
|
49
|
-
obj.Set("peerId", left->PeerId);
|
|
50
|
-
break;
|
|
51
|
-
}
|
|
52
|
-
case OdinEvent_PeerUserDataChanged: {
|
|
53
|
-
const PeerUserDataChangedEventData *changed = static_cast<const PeerUserDataChangedEventData*>(event);
|
|
54
|
-
obj.Set("peerId", changed->PeerId);
|
|
55
|
-
Napi::ArrayBuffer buffer = Napi::ArrayBuffer::New(env, (void*)changed->Data, changed->DataLen);
|
|
56
|
-
Napi::Uint8Array array = Napi::Uint8Array::New(env, changed->DataLen, buffer, 0, napi_uint8_array);
|
|
57
|
-
obj.Set("userData", array);
|
|
58
|
-
break;
|
|
59
|
-
}
|
|
60
|
-
case OdinEvent_RoomUserDataChanged: {
|
|
61
|
-
const RoomUserDataChangedEventData *changed = static_cast<const RoomUserDataChangedEventData*>(event);
|
|
62
|
-
Napi::ArrayBuffer buffer = Napi::ArrayBuffer::New(env, (void*)changed->Data, changed->DataLen);
|
|
63
|
-
Napi::Uint8Array array = Napi::Uint8Array::New(env, changed->DataLen, buffer, 0, napi_uint8_array);
|
|
64
|
-
obj.Set("userData", array);
|
|
65
|
-
break;
|
|
66
|
-
}
|
|
67
|
-
case OdinEvent_MessageReceived: {
|
|
68
|
-
const MessageReceivedEventData *message = static_cast<const MessageReceivedEventData*>(event);
|
|
69
|
-
obj.Set("peerId", message->PeerId);
|
|
70
|
-
Napi::ArrayBuffer buffer = Napi::ArrayBuffer::New(env, (void*)message->Data, message->DataLen);
|
|
71
|
-
Napi::Uint8Array array = Napi::Uint8Array::New(env, message->DataLen, buffer, 0, napi_uint8_array);
|
|
72
|
-
obj.Set("messageData", array);
|
|
73
|
-
break;
|
|
74
|
-
}
|
|
75
|
-
case OdinEvent_RoomConnectionStateChanged: {
|
|
76
|
-
const RoomConnectionStateChangedEventData *state = static_cast<const RoomConnectionStateChangedEventData*>(event);
|
|
77
|
-
obj.Set("state", state->StateName);
|
|
78
|
-
obj.Set("reason", state->ReasonName);
|
|
79
|
-
break;
|
|
80
|
-
}
|
|
81
|
-
case OdinEvent_MediaAdded: {
|
|
82
|
-
const MediaAddedEventData *added = static_cast<const MediaAddedEventData*>(event);
|
|
83
|
-
obj.Set("peerId", added->PeerId);
|
|
84
|
-
obj.Set("mediaId", added->MediaId);
|
|
85
|
-
break;
|
|
86
|
-
}
|
|
87
|
-
case OdinEvent_MediaRemoved: {
|
|
88
|
-
const MediaRemovedEventData *removed = static_cast<const MediaRemovedEventData*>(event);
|
|
89
|
-
obj.Set("peerId", removed->PeerId);
|
|
90
|
-
obj.Set("mediaId", removed->MediaId);
|
|
91
|
-
break;
|
|
92
|
-
}
|
|
93
|
-
case OdinEvent_MediaActiveStateChanged: {
|
|
94
|
-
const MediaActiveStateChangedEventData *activity = static_cast<const MediaActiveStateChangedEventData*>(event);
|
|
95
|
-
obj.Set("peerId", activity->PeerId);
|
|
96
|
-
obj.Set("mediaId", activity->MediaId);
|
|
97
|
-
obj.Set("state", activity->Active);
|
|
98
|
-
break;
|
|
31
|
+
void OdinRoomWrapper::IncrementRoomCount() {
|
|
32
|
+
std::lock_guard<std::mutex> lock(g_sdkLifecycleMutex);
|
|
33
|
+
int prevCount = g_activeRoomCount.fetch_add(1);
|
|
34
|
+
|
|
35
|
+
if (prevCount == 0 && !g_odinSdkInitialized) {
|
|
36
|
+
// First room - initialize the SDK
|
|
37
|
+
OdinError initRc = odin_initialize(ODIN_VERSION);
|
|
38
|
+
if (!odin_is_error(initRc)) {
|
|
39
|
+
g_odinSdkInitialized = true;
|
|
99
40
|
}
|
|
100
41
|
}
|
|
101
|
-
|
|
102
|
-
return obj;
|
|
103
42
|
}
|
|
104
43
|
|
|
105
44
|
/**
|
|
106
|
-
*
|
|
107
|
-
*
|
|
45
|
+
* Decrements the active room count and shuts down the SDK if this was the last room.
|
|
46
|
+
* Thread-safe for concurrent room destruction.
|
|
108
47
|
*/
|
|
109
|
-
|
|
110
|
-
|
|
48
|
+
void OdinRoomWrapper::DecrementRoomCount() {
|
|
49
|
+
std::lock_guard<std::mutex> lock(g_sdkLifecycleMutex);
|
|
50
|
+
int newCount = g_activeRoomCount.fetch_sub(1) - 1;
|
|
51
|
+
|
|
52
|
+
if (newCount == 0 && g_odinSdkInitialized) {
|
|
53
|
+
// Last room closed - shutdown the SDK
|
|
54
|
+
// First free the connection pool if it exists
|
|
55
|
+
if (_pool) {
|
|
56
|
+
odin_connection_pool_free(_pool);
|
|
57
|
+
_pool = nullptr;
|
|
58
|
+
}
|
|
59
|
+
odin_shutdown();
|
|
60
|
+
g_odinSdkInitialized = false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
111
63
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
64
|
+
struct OdinConnectionPool* OdinRoomWrapper::GetConnectionPool() {
|
|
65
|
+
if (!_pool) {
|
|
66
|
+
// Auto-initialize ODIN SDK if not already done
|
|
67
|
+
if (!g_odinSdkInitialized) {
|
|
68
|
+
OdinError initRc = odin_initialize(ODIN_VERSION);
|
|
69
|
+
if (!odin_is_error(initRc)) {
|
|
70
|
+
g_odinSdkInitialized = true;
|
|
71
|
+
} else {
|
|
72
|
+
// Don't print error for "already initialized" - this is expected
|
|
73
|
+
// when OdinClient was created first
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
OdinConnectionPoolSettings settings;
|
|
78
|
+
memset(&settings, 0, sizeof(settings));
|
|
79
|
+
settings.on_datagram = OdinRoomWrapper::OnDatagram;
|
|
80
|
+
settings.on_rpc = OdinRoomWrapper::OnRPC;
|
|
81
|
+
settings.user_data = nullptr;
|
|
82
|
+
|
|
83
|
+
OdinError rc = odin_connection_pool_create(settings, &_pool);
|
|
84
|
+
if (odin_is_error(rc)) {
|
|
85
|
+
printf("Odin NodeJS Addon: Error creating connection pool: %d. Did you forget to create OdinClient first?\n", rc);
|
|
86
|
+
}
|
|
118
87
|
}
|
|
88
|
+
return _pool;
|
|
89
|
+
}
|
|
119
90
|
|
|
120
|
-
|
|
91
|
+
void OdinRoomWrapper::OnDatagram(uint64_t room_ref, uint16_t media_id, const uint8_t *bytes, uint32_t bytes_length, void *user_data) {
|
|
92
|
+
std::lock_guard<std::mutex> lock(_roomsMapMutex);
|
|
93
|
+
auto it = _roomsMap.find(room_ref);
|
|
94
|
+
if (it != _roomsMap.end()) {
|
|
95
|
+
it->second->HandleDatagramInternal(media_id, bytes, bytes_length);
|
|
96
|
+
}
|
|
121
97
|
}
|
|
122
98
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
99
|
+
void OdinRoomWrapper::OnRPC(uint64_t room_ref, const uint8_t *bytes, uint32_t bytes_length, void *user_data) {
|
|
100
|
+
std::lock_guard<std::mutex> lock(_roomsMapMutex);
|
|
101
|
+
auto it = _roomsMap.find(room_ref);
|
|
102
|
+
if (it != _roomsMap.end()) {
|
|
103
|
+
it->second->HandleRPCInternal(bytes, bytes_length);
|
|
104
|
+
}
|
|
128
105
|
}
|
|
129
106
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
return
|
|
107
|
+
Napi::Value OdinRoomWrapper::RoomId(const Napi::CallbackInfo &info) {
|
|
108
|
+
Napi::Env env = info.Env();
|
|
109
|
+
if (!_room) return env.Null();
|
|
110
|
+
char name[256];
|
|
111
|
+
uint32_t len = sizeof(name);
|
|
112
|
+
OdinError err = odin_room_get_name(_room, name, &len);
|
|
113
|
+
if (odin_is_error(err)) return env.Null();
|
|
114
|
+
return Napi::String::New(env, name);
|
|
138
115
|
}
|
|
139
116
|
|
|
140
117
|
/**
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
118
|
+
* Internal handler for audio datagrams received from the ODIN server.
|
|
119
|
+
* Called from the static OnDatagram callback on the connection pool's thread.
|
|
120
|
+
*
|
|
121
|
+
* Thread Safety: Checks _started before accessing decoders to prevent
|
|
122
|
+
* operations on freed decoders during shutdown.
|
|
145
123
|
*/
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
PeerJoinedEventData* data = new PeerJoinedEventData();
|
|
163
|
-
data->Tag = event->tag;
|
|
164
|
-
data->Event = "PeerJoined";
|
|
165
|
-
data->PeerId = event->peer_joined.peer_id;
|
|
166
|
-
data->UserId = event->peer_joined.user_id;
|
|
167
|
-
if (event->peer_joined.peer_user_data && event->peer_joined.peer_user_data_len > 0) {
|
|
168
|
-
data->SetData(event->peer_joined.peer_user_data, event->peer_joined.peer_user_data_len);
|
|
169
|
-
}
|
|
170
|
-
return data;
|
|
171
|
-
}
|
|
172
|
-
case OdinEvent_PeerLeft: {
|
|
173
|
-
PeerLeftEventData* data = new PeerLeftEventData();
|
|
174
|
-
data->Tag = event->tag;
|
|
175
|
-
data->Event = "PeerLeft";
|
|
176
|
-
data->PeerId = event->peer_left.peer_id;
|
|
177
|
-
return data;
|
|
178
|
-
}
|
|
179
|
-
case OdinEvent_PeerUserDataChanged: {
|
|
180
|
-
PeerUserDataChangedEventData* data = new PeerUserDataChangedEventData();
|
|
181
|
-
data->Tag = event->tag;
|
|
182
|
-
data->Event = "PeerUserDataChanged";
|
|
183
|
-
data->PeerId = event->peer_user_data_changed.peer_id;
|
|
184
|
-
if (event->peer_user_data_changed.peer_user_data && event->peer_user_data_changed.peer_user_data_len > 0) {
|
|
185
|
-
data->SetData(event->peer_user_data_changed.peer_user_data, event->peer_user_data_changed.peer_user_data_len);
|
|
186
|
-
}
|
|
187
|
-
return data;
|
|
188
|
-
}
|
|
189
|
-
case OdinEvent_RoomUserDataChanged: {
|
|
190
|
-
RoomUserDataChangedEventData* data = new RoomUserDataChangedEventData();
|
|
191
|
-
data->Tag = event->tag;
|
|
192
|
-
data->Event = "RoomUserDataChanged";
|
|
193
|
-
if (event->room_user_data_changed.room_user_data && event->room_user_data_changed.room_user_data_len > 0) {
|
|
194
|
-
data->SetData(event->room_user_data_changed.room_user_data, event->room_user_data_changed.room_user_data_len);
|
|
195
|
-
}
|
|
196
|
-
return data;
|
|
124
|
+
void OdinRoomWrapper::HandleDatagramInternal(uint16_t media_id, const uint8_t *bytes, uint32_t bytes_length) {
|
|
125
|
+
// Early exit if room is closing - decoders may be freed
|
|
126
|
+
if (!_started) return;
|
|
127
|
+
|
|
128
|
+
std::lock_guard<std::mutex> lock(_decodersMutex);
|
|
129
|
+
|
|
130
|
+
// Double-check after acquiring lock
|
|
131
|
+
if (!_started) return;
|
|
132
|
+
|
|
133
|
+
auto it = _decoders.find(media_id);
|
|
134
|
+
if (it == _decoders.end()) {
|
|
135
|
+
OdinDecoder* decoder = nullptr;
|
|
136
|
+
OdinError rc = odin_decoder_create(media_id, 48000, true, &decoder);
|
|
137
|
+
if (!odin_is_error(rc)) {
|
|
138
|
+
_decoders[media_id] = decoder;
|
|
139
|
+
it = _decoders.find(media_id);
|
|
197
140
|
}
|
|
198
|
-
case OdinEvent_MessageReceived: {
|
|
199
|
-
MessageReceivedEventData* data = new MessageReceivedEventData();
|
|
200
|
-
data->Tag = event->tag;
|
|
201
|
-
data->Event = "MessageReceived";
|
|
202
|
-
data->PeerId = event->message_received.peer_id;
|
|
203
|
-
if (event->message_received.data && event->message_received.data_len > 0) {
|
|
204
|
-
data->SetData(event->message_received.data, event->message_received.data_len);
|
|
205
|
-
}
|
|
206
|
-
return data;
|
|
207
|
-
}
|
|
208
|
-
case OdinEvent_RoomConnectionStateChanged: {
|
|
209
|
-
RoomConnectionStateChangedEventData* data = new RoomConnectionStateChangedEventData();
|
|
210
|
-
data->Tag = event->tag;
|
|
211
|
-
data->Event = "ConnectionStateChanged";
|
|
212
|
-
data->State = (int)event->room_connection_state_changed.state;
|
|
213
|
-
data->Reason = (int)event->room_connection_state_changed.reason;
|
|
214
|
-
data->StateName = OdinUtilities::GetNameFromConnectionState(event->room_connection_state_changed.state);
|
|
215
|
-
data->ReasonName = OdinUtilities::GetNameFromConnectionStateChangeReason(event->room_connection_state_changed.reason);
|
|
216
|
-
return data;
|
|
217
|
-
}
|
|
218
|
-
case OdinEvent_MediaAdded: {
|
|
219
|
-
MediaAddedEventData* data = new MediaAddedEventData();
|
|
220
|
-
data->Tag = event->tag;
|
|
221
|
-
data->Event = "MediaAdded";
|
|
222
|
-
data->PeerId = event->media_added.peer_id;
|
|
223
|
-
data->MediaId = GetMediaIdFromHandle(event->media_added.media_handle);
|
|
224
|
-
data->MediaStreamHandle = event->media_added.media_handle;
|
|
225
|
-
return data;
|
|
226
|
-
}
|
|
227
|
-
case OdinEvent_MediaRemoved: {
|
|
228
|
-
MediaRemovedEventData* data = new MediaRemovedEventData();
|
|
229
|
-
data->Tag = event->tag;
|
|
230
|
-
data->Event = "MediaRemoved";
|
|
231
|
-
data->PeerId = event->media_removed.peer_id;
|
|
232
|
-
data->MediaId = GetMediaIdFromHandle(event->media_removed.media_handle);
|
|
233
|
-
return data;
|
|
234
|
-
}
|
|
235
|
-
case OdinEvent_MediaActiveStateChanged: {
|
|
236
|
-
MediaActiveStateChangedEventData *data = new MediaActiveStateChangedEventData();
|
|
237
|
-
data->Tag = event->tag;
|
|
238
|
-
data->Event = "MediaActivity";
|
|
239
|
-
data->PeerId = event->media_active_state_changed.peer_id;
|
|
240
|
-
data->MediaId = GetMediaIdFromHandle(event->media_active_state_changed.media_handle);
|
|
241
|
-
data->Active = event->media_active_state_changed.active;
|
|
242
|
-
return data;
|
|
243
|
-
}
|
|
244
|
-
default:
|
|
245
|
-
return NULL;
|
|
246
141
|
}
|
|
247
|
-
|
|
248
|
-
|
|
142
|
+
|
|
143
|
+
if (it != _decoders.end()) {
|
|
144
|
+
odin_decoder_push(it->second, bytes, bytes_length);
|
|
145
|
+
}
|
|
249
146
|
}
|
|
250
147
|
|
|
251
148
|
/**
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
*
|
|
149
|
+
* Internal handler for RPC messages received from the ODIN server.
|
|
150
|
+
* Called from the static OnRPC callback on the connection pool's thread.
|
|
151
|
+
*
|
|
152
|
+
* Thread Safety: Checks _started before calling callbacks to prevent
|
|
153
|
+
* calling released ThreadSafeFunctions during shutdown.
|
|
257
154
|
*/
|
|
258
|
-
void
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
155
|
+
void OdinRoomWrapper::HandleRPCInternal(const uint8_t *bytes, uint32_t bytes_length) {
|
|
156
|
+
// Early exit if room is closing - callbacks may be released
|
|
157
|
+
if (!_started) return;
|
|
158
|
+
|
|
159
|
+
// Just pass the raw RPC bytes to JavaScript
|
|
160
|
+
// The JS layer can handle parsing MessagePack format
|
|
161
|
+
|
|
162
|
+
// Create a copy of the data to pass to the callback
|
|
163
|
+
std::vector<uint8_t>* data = new std::vector<uint8_t>(bytes, bytes + bytes_length);
|
|
164
|
+
|
|
165
|
+
// Get pointer to pending counter for ref-counting
|
|
166
|
+
std::atomic<int>* pendingPtr = &_pendingCallbacks;
|
|
167
|
+
|
|
168
|
+
auto callback = [pendingPtr](Napi::Env env, Napi::Function jsCallback, void* value) {
|
|
169
|
+
std::vector<uint8_t>* rpcData = (std::vector<uint8_t>*)value;
|
|
170
|
+
try {
|
|
171
|
+
auto buffer = Napi::ArrayBuffer::New(env, rpcData->size());
|
|
172
|
+
std::copy(rpcData->begin(), rpcData->end(), (uint8_t*)buffer.Data());
|
|
173
|
+
jsCallback.Call({buffer});
|
|
174
|
+
} catch (...) {
|
|
175
|
+
// Exception in RPC callback - silently ignore
|
|
274
176
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
// This should never happen anyway
|
|
281
|
-
jsCallback.Call({Napi::Number::New( env, 42 )});
|
|
282
|
-
} else {
|
|
283
|
-
Napi::Object obj = PrepareEventObject(env, eventData);
|
|
284
|
-
jsCallback.Call( {obj} );
|
|
285
|
-
delete eventData;
|
|
286
|
-
}
|
|
287
|
-
};
|
|
288
|
-
if (odinRoom->_eventListener != NULL)
|
|
289
|
-
{
|
|
290
|
-
EventData* data = odinRoom->PrepareEventData((OdinEvent*)event);
|
|
291
|
-
#ifdef DEBUG
|
|
292
|
-
printf("Odin NodeJS Addon: Sending event to JS: %s\n", data->Event.c_str());
|
|
293
|
-
#endif
|
|
294
|
-
odinRoom->_eventListener.BlockingCall( (void*)data, callback );
|
|
177
|
+
delete rpcData;
|
|
178
|
+
|
|
179
|
+
// Decrement pending count AFTER callback completes
|
|
180
|
+
if (pendingPtr) {
|
|
181
|
+
pendingPtr->fetch_sub(1);
|
|
295
182
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
// Emit to the generic event listener (with safety check)
|
|
186
|
+
if (_started && _eventListener) {
|
|
187
|
+
std::vector<uint8_t>* dataCopy = new std::vector<uint8_t>(*data);
|
|
188
|
+
_pendingCallbacks.fetch_add(1); // Increment before NonBlockingCall
|
|
189
|
+
_eventListener.NonBlockingCall(dataCopy, callback);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Emit to specific event listeners (with safety check for each)
|
|
193
|
+
// Note: We iterate over a copy of the keys to avoid issues if the map changes
|
|
194
|
+
if (_started) {
|
|
195
|
+
for (auto& pair : _eventListeners) {
|
|
196
|
+
if (!_started) break; // Early exit if closing during iteration
|
|
197
|
+
std::vector<uint8_t>* dataCopy = new std::vector<uint8_t>(*data);
|
|
198
|
+
_pendingCallbacks.fetch_add(1); // Increment before NonBlockingCall
|
|
199
|
+
pair.second.NonBlockingCall(dataCopy, callback);
|
|
301
200
|
}
|
|
302
|
-
|
|
303
|
-
} else {
|
|
304
|
-
printf("Odin NodeJS Addon: No room available when handling events - this should NEVER HAPPEN.\n");
|
|
305
201
|
}
|
|
202
|
+
|
|
203
|
+
delete data;
|
|
306
204
|
}
|
|
307
205
|
|
|
308
|
-
|
|
309
|
-
* Creates a new OdinRoom instance. Requires an Odin token as a parameter.
|
|
310
|
-
* @param info Napi::CallbackInfo
|
|
311
|
-
*/
|
|
312
|
-
OdinRoom::OdinRoom(const Napi::CallbackInfo& info) :Napi::ObjectWrap<OdinRoom>(info) {
|
|
206
|
+
OdinRoomWrapper::OdinRoomWrapper(const Napi::CallbackInfo& info) : Napi::ObjectWrap<OdinRoomWrapper>(info) {
|
|
313
207
|
Napi::Env env = info.Env();
|
|
314
|
-
|
|
315
208
|
if (info.Length() != 1 || !info[0].IsString()) {
|
|
316
209
|
Napi::TypeError::New(env, "Provide a token to create a room").ThrowAsJavaScriptException();
|
|
317
210
|
return;
|
|
318
211
|
}
|
|
319
|
-
|
|
212
|
+
|
|
213
|
+
// Increment room count - initializes SDK if this is the first room
|
|
214
|
+
IncrementRoomCount();
|
|
215
|
+
|
|
320
216
|
_token = info[0].ToString().Utf8Value();
|
|
321
|
-
|
|
322
|
-
|
|
217
|
+
_room = nullptr;
|
|
218
|
+
_cipher = nullptr;
|
|
323
219
|
_joined = false;
|
|
220
|
+
_started = false;
|
|
221
|
+
_pendingCallbacks = 0;
|
|
222
|
+
_ownPeerId = 0;
|
|
223
|
+
// Initialize position to origin and scale to 1.0 (default)
|
|
224
|
+
_position[0] = 0.0f;
|
|
225
|
+
_position[1] = 0.0f;
|
|
226
|
+
_position[2] = 0.0f;
|
|
227
|
+
_positionScale = 1.0f;
|
|
228
|
+
|
|
324
229
|
|
|
325
|
-
odin_room_set_event_callback(_roomHandle, OdinRoom::HandleOdinEvent, this);
|
|
326
230
|
}
|
|
327
231
|
|
|
328
232
|
/**
|
|
329
|
-
*
|
|
233
|
+
* Releases resources when the JavaScript object is garbage collected.
|
|
234
|
+
*
|
|
235
|
+
* This ensures resources are freed even if close() wasn't called explicitly.
|
|
236
|
+
* Since Close() now handles all cleanup, Finalize() just needs to:
|
|
237
|
+
* 1. Stop the thread and free room (if not already done)
|
|
238
|
+
* 2. Free any remaining decoders
|
|
239
|
+
* 3. Decrement room count for SDK lifecycle management
|
|
330
240
|
*/
|
|
331
|
-
void
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
241
|
+
void OdinRoomWrapper::Finalize(Napi::Env env) {
|
|
242
|
+
// Stop audio thread if still running
|
|
243
|
+
_started = false;
|
|
244
|
+
if (_nativeThread.joinable()) {
|
|
245
|
+
_nativeThread.join();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Free decoders if not already done by Close()
|
|
249
|
+
{
|
|
250
|
+
std::lock_guard<std::mutex> lock(_decodersMutex);
|
|
251
|
+
for (auto& pair : _decoders) {
|
|
252
|
+
odin_decoder_free(pair.second);
|
|
253
|
+
}
|
|
254
|
+
_decoders.clear();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Clear media-to-peer mapping
|
|
258
|
+
{
|
|
259
|
+
std::lock_guard<std::mutex> lock(_mediaToPeerMutex);
|
|
260
|
+
_mediaToPeer.clear();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Free room if not already done by Close()
|
|
264
|
+
if (_room) {
|
|
265
|
+
{
|
|
266
|
+
std::lock_guard<std::mutex> lock(_roomsMapMutex);
|
|
267
|
+
_roomsMap.erase(odin_room_get_ref(_room));
|
|
268
|
+
}
|
|
269
|
+
odin_room_free(_room);
|
|
270
|
+
_room = nullptr;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Clear cipher reference
|
|
274
|
+
_cipher = nullptr;
|
|
275
|
+
|
|
276
|
+
// Decrement room count - shuts down SDK if this was the last room
|
|
277
|
+
DecrementRoomCount();
|
|
335
278
|
}
|
|
336
279
|
|
|
337
|
-
|
|
338
|
-
* Exposes this class to JavaScript
|
|
339
|
-
* @param info Napi::CallbackInfo
|
|
340
|
-
*/
|
|
341
|
-
Napi::Object OdinRoom::Init(Napi::Env env, Napi::Object exports) {
|
|
342
|
-
// This method is used to hook the accessor and method callbacks
|
|
280
|
+
Napi::Object OdinRoomWrapper::Init(Napi::Env env, Napi::Object exports) {
|
|
343
281
|
Napi::Function func = DefineClass(env, "OdinRoom", {
|
|
344
|
-
InstanceMethod<&
|
|
345
|
-
InstanceMethod<&
|
|
346
|
-
InstanceMethod<&
|
|
347
|
-
InstanceMethod<&
|
|
348
|
-
InstanceMethod<&
|
|
349
|
-
InstanceMethod<&
|
|
350
|
-
InstanceMethod<&
|
|
351
|
-
InstanceMethod<&
|
|
352
|
-
InstanceMethod<&
|
|
353
|
-
InstanceMethod<&
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
282
|
+
InstanceMethod<&OdinRoomWrapper::Join>("join", static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
|
|
283
|
+
InstanceMethod<&OdinRoomWrapper::UpdatePeerUserData>("updateOwnUserData", static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
|
|
284
|
+
InstanceMethod<&OdinRoomWrapper::UpdatePosition>("updatePosition", static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
|
|
285
|
+
InstanceMethod<&OdinRoomWrapper::SetPositionScale>("setPositionScale", static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
|
|
286
|
+
InstanceMethod<&OdinRoomWrapper::SendMessage>("sendMessage", static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
|
|
287
|
+
InstanceMethod<&OdinRoomWrapper::SendRpc>("sendRpc", static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
|
|
288
|
+
InstanceMethod<&OdinRoomWrapper::SetEventListener>("setEventListener", static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
|
|
289
|
+
InstanceMethod<&OdinRoomWrapper::AddEventListener>("addEventListener", static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
|
|
290
|
+
InstanceMethod<&OdinRoomWrapper::RemoveEventListener>("removeEventListener", static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
|
|
291
|
+
InstanceMethod<&OdinRoomWrapper::Close>("close", static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
|
|
292
|
+
InstanceMethod<&OdinRoomWrapper::CreateAudioStream>("createAudioStream", static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
|
|
293
|
+
InstanceMethod<&OdinRoomWrapper::SetCipher>("setCipher", static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
|
|
294
|
+
InstanceMethod<&OdinRoomWrapper::RegisterMediaPeer>("registerMediaPeer", static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
|
|
295
|
+
InstanceMethod<&OdinRoomWrapper::UnregisterMedia>("unregisterMedia", static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
|
|
296
|
+
InstanceMethod<&OdinRoomWrapper::SetOwnPeerId>("setOwnPeerId", static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
|
|
297
|
+
InstanceMethod<&OdinRoomWrapper::GetConnectionStats>("getConnectionStats", static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
|
|
298
|
+
InstanceMethod<&OdinRoomWrapper::GetConnectionId>("getConnectionId", static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
|
|
299
|
+
InstanceMethod<&OdinRoomWrapper::GetJitterStats>("getJitterStats", static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
|
|
300
|
+
InstanceAccessor("ownPeerId", &OdinRoomWrapper::GetOwnPeerId, nullptr, static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
|
|
301
|
+
InstanceAccessor("id", &OdinRoomWrapper::RoomId, nullptr, static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
|
|
302
|
+
StaticMethod<&OdinRoomWrapper::CreateNewItem>("CreateNewItem", static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
|
|
357
303
|
});
|
|
358
304
|
|
|
359
|
-
// We use a static variable to store the constructor. This is because we need to create instances within C++ and
|
|
360
|
-
// expose to JavaScript. We can't use the constructor directly because it's not exposed to JavaScript.
|
|
361
305
|
constructor = new Napi::FunctionReference();
|
|
362
|
-
|
|
363
|
-
// Create a persistent reference to the class constructor. This will allow
|
|
364
|
-
// a function called on a class prototype and a function
|
|
365
|
-
// called on instance of a class to be distinguished from each other.
|
|
366
306
|
*constructor = Napi::Persistent(func);
|
|
367
307
|
exports.Set("OdinRoom", func);
|
|
368
|
-
|
|
369
|
-
// Store the constructor as the add-on instance data. This will allow this
|
|
370
|
-
// add-on to support multiple instances of itself running on multiple worker
|
|
371
|
-
// threads, as well as multiple instances of itself running in different
|
|
372
|
-
// contexts on the same thread.
|
|
373
|
-
//
|
|
374
|
-
// By default, the value set on the environment here will be destroyed when
|
|
375
|
-
// the add-on is unloaded using the `delete` operator, but it is also
|
|
376
|
-
// possible to supply a custom deleter.
|
|
377
|
-
env.SetInstanceData<Napi::FunctionReference>(constructor);
|
|
378
|
-
|
|
308
|
+
// REMOVED: env.SetInstanceData<Napi::FunctionReference>(constructor);
|
|
379
309
|
return exports;
|
|
380
310
|
}
|
|
381
311
|
|
|
382
312
|
/**
|
|
383
|
-
* Updates the position of
|
|
384
|
-
*
|
|
313
|
+
* Updates the 3D position of the local peer. This position is used for spatial audio
|
|
314
|
+
* and server-side culling. If called before join(), the position will be used during
|
|
315
|
+
* room creation. If called after join(), this is a no-op as the current SDK doesn't
|
|
316
|
+
* support runtime position updates (position is only set during room creation).
|
|
317
|
+
*
|
|
318
|
+
* @param info[0] x coordinate (number)
|
|
319
|
+
* @param info[1] y coordinate (number)
|
|
320
|
+
* @param info[2] z coordinate (number)
|
|
385
321
|
*/
|
|
386
|
-
void
|
|
387
|
-
{
|
|
322
|
+
void OdinRoomWrapper::UpdatePosition(const Napi::CallbackInfo &info) {
|
|
388
323
|
Napi::Env env = info.Env();
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
324
|
+
|
|
325
|
+
// Validate arguments: we need exactly 3 numbers (x, y, z)
|
|
326
|
+
if (info.Length() < 3) {
|
|
327
|
+
Napi::TypeError::New(env, "updatePosition requires 3 arguments: x, y, z").ThrowAsJavaScriptException();
|
|
392
328
|
return;
|
|
393
329
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
OdinReturnCode error = odin_room_update_position(_roomHandle, x, y, z);
|
|
400
|
-
if (odin_is_error(error))
|
|
401
|
-
{
|
|
402
|
-
OdinUtilities::ThrowNapiException(env, error, "Failed to update position");
|
|
330
|
+
|
|
331
|
+
if (!info[0].IsNumber() || !info[1].IsNumber() || !info[2].IsNumber()) {
|
|
332
|
+
Napi::TypeError::New(env, "updatePosition arguments must be numbers").ThrowAsJavaScriptException();
|
|
333
|
+
return;
|
|
403
334
|
}
|
|
335
|
+
|
|
336
|
+
// Store the position values (will be used during join/room creation)
|
|
337
|
+
// Note: The ODIN SDK 1.8.2+ only supports setting position during odin_room_create_ex,
|
|
338
|
+
// so this stores the values for use before join() is called.
|
|
339
|
+
_position[0] = info[0].As<Napi::Number>().FloatValue();
|
|
340
|
+
_position[1] = info[1].As<Napi::Number>().FloatValue();
|
|
341
|
+
_position[2] = info[2].As<Napi::Number>().FloatValue();
|
|
404
342
|
}
|
|
405
343
|
|
|
406
344
|
/**
|
|
407
|
-
* Sets the
|
|
408
|
-
*
|
|
345
|
+
* Sets the scaling factor for position coordinates. This scale is applied to the
|
|
346
|
+
* position values when they are used during room creation. Peers are visible to each
|
|
347
|
+
* other within a unit circle of radius 1.0, so the scale should be set such that
|
|
348
|
+
* the maximum distance between peers remains <= 1.0.
|
|
349
|
+
*
|
|
350
|
+
* @param info[0] scale factor (number)
|
|
409
351
|
*/
|
|
410
|
-
void
|
|
411
|
-
{
|
|
352
|
+
void OdinRoomWrapper::SetPositionScale(const Napi::CallbackInfo &info) {
|
|
412
353
|
Napi::Env env = info.Env();
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
354
|
+
|
|
355
|
+
// Validate arguments: we need exactly 1 number (scale)
|
|
356
|
+
if (info.Length() < 1 || !info[0].IsNumber()) {
|
|
357
|
+
Napi::TypeError::New(env, "setPositionScale requires a scale value (number)").ThrowAsJavaScriptException();
|
|
416
358
|
return;
|
|
417
359
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
OdinReturnCode error = odin_room_set_position_scale(_roomHandle, scale);
|
|
422
|
-
if (odin_is_error(error))
|
|
423
|
-
{
|
|
424
|
-
OdinUtilities::ThrowNapiException(env, error, "Failed to set position scale");
|
|
425
|
-
}
|
|
360
|
+
|
|
361
|
+
// Store the scale value (will be applied to position during join/room creation)
|
|
362
|
+
_positionScale = info[0].As<Napi::Number>().FloatValue();
|
|
426
363
|
}
|
|
427
364
|
|
|
428
|
-
|
|
429
|
-
* Joins the Odin room. Requires a gateway URL as a parameter and optionally initial peer data.
|
|
430
|
-
* @param info Napi::CallbackInfo
|
|
431
|
-
*/
|
|
432
|
-
void OdinRoom::Join(const Napi::CallbackInfo &info) {
|
|
365
|
+
void OdinRoomWrapper::Join(const Napi::CallbackInfo &info) {
|
|
433
366
|
Napi::Env env = info.Env();
|
|
434
|
-
|
|
435
367
|
if (info.Length() < 1) {
|
|
436
|
-
Napi::TypeError::New(env, "Gateway required
|
|
368
|
+
Napi::TypeError::New(env, "Gateway required").ThrowAsJavaScriptException();
|
|
437
369
|
return;
|
|
438
370
|
}
|
|
439
|
-
|
|
440
371
|
std::string url = info[0].ToString().Utf8Value();
|
|
441
|
-
|
|
442
|
-
|
|
372
|
+
const uint8_t* userData = nullptr;
|
|
373
|
+
size_t userDataLen = 0;
|
|
374
|
+
if (info.Length() > 1 && info[1].IsTypedArray()) {
|
|
443
375
|
Napi::Uint8Array data = info[1].As<Napi::Uint8Array>();
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
376
|
+
userData = data.Data();
|
|
377
|
+
userDataLen = data.ByteLength();
|
|
378
|
+
}
|
|
379
|
+
struct OdinConnectionPool* pool = GetConnectionPool();
|
|
380
|
+
// Apply position scale to the stored position coordinates
|
|
381
|
+
float scaledPosition[3] = {
|
|
382
|
+
_position[0] / _positionScale,
|
|
383
|
+
_position[1] / _positionScale,
|
|
384
|
+
_position[2] / _positionScale
|
|
385
|
+
};
|
|
386
|
+
OdinError error = odin_room_create_ex(pool, url.c_str(), _token.c_str(), nullptr, userData, userDataLen, scaledPosition, _cipher, &_room);
|
|
387
|
+
if (odin_is_error(error)) {
|
|
454
388
|
OdinUtilities::ThrowNapiException(env, error, "Failed to join room");
|
|
455
389
|
return;
|
|
456
390
|
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
// Start capturing audio data if event listener has not been started yet
|
|
461
|
-
if (_audioDataReceivedEventListener) {
|
|
462
|
-
HandleAudioData();
|
|
391
|
+
{
|
|
392
|
+
std::lock_guard<std::mutex> lock(_roomsMapMutex);
|
|
393
|
+
_roomsMap[odin_room_get_ref(_room)] = this;
|
|
463
394
|
}
|
|
395
|
+
_joined = true;
|
|
396
|
+
_started = true;
|
|
397
|
+
_nativeThread = std::thread(&OdinRoomWrapper::HandleAudioData, this);
|
|
464
398
|
}
|
|
465
399
|
|
|
466
400
|
/**
|
|
467
|
-
* Closes the room connection and
|
|
468
|
-
*
|
|
401
|
+
* Closes the room connection and frees all associated resources.
|
|
402
|
+
*
|
|
403
|
+
* This method performs a complete cleanup in the correct order to prevent:
|
|
404
|
+
* - Memory leaks (decoders, media-to-peer mappings)
|
|
405
|
+
* - Dangling pointers (cipher reference)
|
|
406
|
+
* - Race conditions (thread-callback synchronization)
|
|
407
|
+
*
|
|
408
|
+
* CRITICAL: The cleanup sequence must:
|
|
409
|
+
* 1. Remove from room map FIRST (stops connection pool from routing new callbacks)
|
|
410
|
+
* 2. Close native room (stops connection pool activity)
|
|
411
|
+
* 3. Signal thread to stop and wait for it
|
|
412
|
+
* 4. THEN release callbacks (now safe - no more incoming calls)
|
|
413
|
+
* 5. Free remaining resources
|
|
469
414
|
*/
|
|
470
|
-
void
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
if (_roomHandle <= 0) return;
|
|
474
|
-
|
|
475
|
-
odin_room_set_event_callback(_roomHandle, NULL, NULL);
|
|
476
|
-
|
|
477
|
-
if (this->_eventListener != NULL) {
|
|
478
|
-
this->_eventListener.Release();
|
|
479
|
-
this->_eventListener = NULL;
|
|
415
|
+
void OdinRoomWrapper::Close(const Napi::CallbackInfo &info) {
|
|
416
|
+
if (!_room || !_joined) {
|
|
417
|
+
return;
|
|
480
418
|
}
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
419
|
+
|
|
420
|
+
uint64_t roomRef = odin_room_get_ref(_room);
|
|
421
|
+
// Step 1: Remove from global room map FIRST
|
|
422
|
+
// This prevents the connection pool from routing any new callbacks to us
|
|
423
|
+
{
|
|
424
|
+
std::lock_guard<std::mutex> lock(_roomsMapMutex);
|
|
425
|
+
_roomsMap.erase(roomRef);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Step 2: Close the native room - this stops the connection pool
|
|
429
|
+
// After this call, OnDatagram and OnRPC won't be called for this room
|
|
430
|
+
odin_room_close(_room);
|
|
431
|
+
|
|
432
|
+
// Step 3: Signal the audio thread to stop
|
|
433
|
+
_started = false;
|
|
434
|
+
|
|
435
|
+
// Step 4: Wait for audio processing thread to finish
|
|
436
|
+
// The thread will see _started=false and exit
|
|
437
|
+
if (_nativeThread.joinable()) {
|
|
438
|
+
_nativeThread.join();
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
// Step 4.5: Wait for pending NonBlockingCall callbacks to complete
|
|
443
|
+
// NonBlockingCall schedules work asynchronously on the Node.js event loop.
|
|
444
|
+
// Even after the audio thread exits, there may be callbacks already queued
|
|
445
|
+
// that haven't executed yet. We must wait for these to complete before
|
|
446
|
+
// releasing the ThreadSafeFunctions.
|
|
447
|
+
//
|
|
448
|
+
// We use ref-counting: _pendingCallbacks is incremented before NonBlockingCall
|
|
449
|
+
// and decremented in the callback after execution. We wait for it to reach 0.
|
|
450
|
+
{
|
|
451
|
+
int waitMs = 0;
|
|
452
|
+
const int maxWaitMs = 500; // Maximum wait time to prevent hangs
|
|
453
|
+
while (_pendingCallbacks.load() > 0 && waitMs < maxWaitMs) {
|
|
454
|
+
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
|
455
|
+
waitMs += 10;
|
|
486
456
|
}
|
|
457
|
+
// If we still have pending callbacks after timeout, they will execute
|
|
458
|
+
// after release but we've done our best. In practice this shouldn't happen.
|
|
487
459
|
}
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
/**
|
|
495
|
-
* Creates a new instance of the OdinRoom class. Requires a token as a parameter.
|
|
496
|
-
* @param info
|
|
497
|
-
* @return
|
|
498
|
-
*/
|
|
499
|
-
Napi::Value OdinRoom::CreateNewItem(const Napi::CallbackInfo& info) {
|
|
500
|
-
Napi::Env env = info.Env();
|
|
501
|
-
|
|
502
|
-
if (info.Length() < 1) {
|
|
503
|
-
Napi::TypeError::New(env, "Token required as first parameter").ThrowAsJavaScriptException();
|
|
504
|
-
return env.Undefined();
|
|
460
|
+
|
|
461
|
+
// Step 5: NOW it's safe to release ThreadSafeFunctions
|
|
462
|
+
// No more connection pool callbacks, no more audio thread activity,
|
|
463
|
+
// and all pending NonBlockingCall callbacks have completed
|
|
464
|
+
if (_eventListener) {
|
|
465
|
+
_eventListener.Release();
|
|
505
466
|
}
|
|
467
|
+
if (_audioDataReceivedEventListener) {
|
|
468
|
+
_audioDataReceivedEventListener.Release();
|
|
469
|
+
}
|
|
470
|
+
for (auto& pair : _eventListeners) {
|
|
471
|
+
pair.second.Release();
|
|
472
|
+
}
|
|
473
|
+
_eventListeners.clear();
|
|
474
|
+
|
|
475
|
+
// Step 6: Free all decoders to prevent memory leaks
|
|
476
|
+
{
|
|
477
|
+
std::lock_guard<std::mutex> lock(_decodersMutex);
|
|
478
|
+
for (auto& pair : _decoders) {
|
|
479
|
+
odin_decoder_free(pair.second);
|
|
480
|
+
}
|
|
481
|
+
_decoders.clear();
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Step 7: Clear media-to-peer mapping
|
|
485
|
+
{
|
|
486
|
+
std::lock_guard<std::mutex> lock(_mediaToPeerMutex);
|
|
487
|
+
_mediaToPeer.clear();
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Step 8: Clear cipher reference (we don't own it, just null the pointer)
|
|
491
|
+
_cipher = nullptr;
|
|
492
|
+
|
|
493
|
+
// Step 9: Free the native room
|
|
494
|
+
odin_room_free(_room);
|
|
495
|
+
_room = nullptr;
|
|
496
|
+
_joined = false;
|
|
497
|
+
|
|
498
|
+
}
|
|
506
499
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
// Retrieve the instance data we stored during `Init()`. We only stored the
|
|
510
|
-
// constructor there, so we retrieve it here to create a new instance of the
|
|
511
|
-
// JS class the constructor represents.
|
|
512
|
-
Napi::FunctionReference* constructor =
|
|
513
|
-
info.Env().GetInstanceData<Napi::FunctionReference>();
|
|
514
|
-
return constructor->New({ token });
|
|
500
|
+
Napi::Value OdinRoomWrapper::CreateNewItem(const Napi::CallbackInfo& info) {
|
|
501
|
+
return constructor->New({ info[0] });
|
|
515
502
|
}
|
|
516
503
|
|
|
517
|
-
|
|
518
|
-
* Creates a new instance of the OdinRoom class. Requires a token as a parameter.
|
|
519
|
-
* @param info
|
|
520
|
-
* @return
|
|
521
|
-
*/
|
|
522
|
-
Napi::Object OdinRoom::NewInstance(Napi::Value arg) {
|
|
504
|
+
Napi::Object OdinRoomWrapper::NewInstance(Napi::Value arg) {
|
|
523
505
|
return constructor->New({ arg });
|
|
524
506
|
}
|
|
525
507
|
|
|
526
|
-
|
|
527
|
-
* Updates the peer user data. Requires an array of bytes as a parameter.
|
|
528
|
-
* @param info
|
|
529
|
-
* @return
|
|
530
|
-
*/
|
|
531
|
-
void OdinRoom::UpdatePeerUserData(const Napi::CallbackInfo &info) {
|
|
532
|
-
Napi::Env env = info.Env();
|
|
533
|
-
|
|
534
|
-
if (info.Length() != 1 || !info[0].IsTypedArray()) {
|
|
535
|
-
Napi::TypeError::New(env, "Data as byte array expected").ThrowAsJavaScriptException();
|
|
536
|
-
return;
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
Napi::Uint8Array data = info[0].As<Napi::Uint8Array>();
|
|
540
|
-
|
|
541
|
-
OdinReturnCode error = odin_room_update_peer_user_data(_roomHandle, data.Data(), data.ByteLength());
|
|
508
|
+
void OdinRoomWrapper::UpdatePeerUserData(const Napi::CallbackInfo &info) {}
|
|
542
509
|
|
|
543
|
-
|
|
544
|
-
{
|
|
545
|
-
OdinUtilities::ThrowNapiException(env, error, "Failed to update peer user data");
|
|
546
|
-
}
|
|
547
|
-
}
|
|
510
|
+
void OdinRoomWrapper::SendMessage(const Napi::CallbackInfo &info) {}
|
|
548
511
|
|
|
549
512
|
/**
|
|
550
|
-
* Sends a
|
|
551
|
-
*
|
|
552
|
-
*
|
|
553
|
-
* @param info
|
|
554
|
-
* @return
|
|
513
|
+
* Sends a raw RPC message (MessagePack format) to the ODIN server.
|
|
514
|
+
* This is used for commands like StartMedia, StopMedia, etc.
|
|
515
|
+
*
|
|
516
|
+
* @param info[0] Uint8Array containing the MessagePack-encoded RPC message
|
|
555
517
|
*/
|
|
556
|
-
void
|
|
518
|
+
void OdinRoomWrapper::SendRpc(const Napi::CallbackInfo &info) {
|
|
557
519
|
Napi::Env env = info.Env();
|
|
558
|
-
|
|
559
|
-
if (
|
|
560
|
-
Napi::
|
|
520
|
+
|
|
521
|
+
if (!_room || !_joined) {
|
|
522
|
+
Napi::Error::New(env, "Room not joined").ThrowAsJavaScriptException();
|
|
561
523
|
return;
|
|
562
524
|
}
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
size_t peer_id_list_length = 0;
|
|
568
|
-
if (info.Length() >= 2 && !info[1].IsNull() && !info[1].IsUndefined()) {
|
|
569
|
-
if (!info[1].IsArray()) {
|
|
570
|
-
Napi::TypeError::New(env, "Peer id list as uint32 array expected").ThrowAsJavaScriptException();
|
|
571
|
-
} else {
|
|
572
|
-
Napi::Array peerIds = info[1].As<Napi::Array>();
|
|
573
|
-
peer_id_list_length = peerIds.Length();
|
|
574
|
-
peer_id_list = new uint64_t[peer_id_list_length];
|
|
575
|
-
for (int i = 0; i < (int)peer_id_list_length; i++) {
|
|
576
|
-
peer_id_list[i] = peerIds.Get(i).As<Napi::Number>().Uint32Value();
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
OdinReturnCode error = odin_room_send_message(_roomHandle, peer_id_list, peer_id_list_length, data.Data(), data.ByteLength());
|
|
582
|
-
|
|
583
|
-
if (peer_id_list != NULL) {
|
|
584
|
-
delete[] peer_id_list;
|
|
525
|
+
|
|
526
|
+
if (info.Length() < 1 || !info[0].IsTypedArray()) {
|
|
527
|
+
Napi::TypeError::New(env, "Uint8Array required as RPC data").ThrowAsJavaScriptException();
|
|
528
|
+
return;
|
|
585
529
|
}
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
530
|
+
|
|
531
|
+
Napi::Uint8Array rpcData = info[0].As<Napi::Uint8Array>();
|
|
532
|
+
|
|
533
|
+
OdinError rc = odin_room_send_rpc(_room, rpcData.Data(), static_cast<uint32_t>(rpcData.ByteLength()));
|
|
534
|
+
if (odin_is_error(rc)) {
|
|
535
|
+
OdinUtilities::ThrowNapiException(env, rc, "Failed to send RPC");
|
|
590
536
|
}
|
|
591
537
|
}
|
|
592
538
|
|
|
593
|
-
|
|
594
|
-
* Sets a global event listener for this room. Requires a callback function as a parameter and will receive all events
|
|
595
|
-
* @param info
|
|
596
|
-
* @return
|
|
597
|
-
*/
|
|
598
|
-
void OdinRoom::SetEventListener(const Napi::CallbackInfo &info) {
|
|
539
|
+
void OdinRoomWrapper::SetEventListener(const Napi::CallbackInfo &info) {
|
|
599
540
|
Napi::Env env = info.Env();
|
|
600
|
-
|
|
601
541
|
if (info.Length() != 1 || !info[0].IsFunction()) {
|
|
602
542
|
Napi::TypeError::New(env, "Callback function expected").ThrowAsJavaScriptException();
|
|
603
543
|
return;
|
|
604
544
|
}
|
|
605
|
-
|
|
606
|
-
Napi::Function napiFunction = info[0].As<Napi::Function>();
|
|
607
|
-
|
|
608
|
-
_eventListener = Napi::ThreadSafeFunction::New(env, napiFunction, "Callback", 0, 1);
|
|
545
|
+
_eventListener = Napi::ThreadSafeFunction::New(env, info[0].As<Napi::Function>(), "Callback", 0, 1);
|
|
609
546
|
}
|
|
610
547
|
|
|
611
|
-
|
|
612
|
-
* Adds an event listener for this room. Requires the event type to listen to and a callback function as a second parameter
|
|
613
|
-
* @param info
|
|
614
|
-
* @return
|
|
615
|
-
*/
|
|
616
|
-
void OdinRoom::AddEventListener(const Napi::CallbackInfo &info) {
|
|
548
|
+
void OdinRoomWrapper::AddEventListener(const Napi::CallbackInfo &info) {
|
|
617
549
|
Napi::Env env = info.Env();
|
|
618
|
-
|
|
619
550
|
if (info.Length() != 2 || !info[0].IsString() || !info[1].IsFunction()) {
|
|
620
|
-
Napi::TypeError::New(env, "Event Name and Callback
|
|
551
|
+
Napi::TypeError::New(env, "Event Name and Callback expected").ThrowAsJavaScriptException();
|
|
621
552
|
return;
|
|
622
553
|
}
|
|
623
|
-
|
|
624
554
|
std::string eventName = info[0].As<Napi::String>().Utf8Value();
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
if (eventName != "AudioDataReceived") {
|
|
628
|
-
_eventListeners[eventName] = Napi::ThreadSafeFunction::New(env, napiFunction, eventName + "Callback", 0, 1);
|
|
555
|
+
if (eventName == "AudioDataReceived") {
|
|
556
|
+
_audioDataReceivedEventListener = Napi::ThreadSafeFunction::New(env, info[1].As<Napi::Function>(), "AudioDataReceivedCallback", 0, 1);
|
|
629
557
|
} else {
|
|
630
|
-
|
|
631
|
-
env,
|
|
632
|
-
napiFunction,
|
|
633
|
-
"AudioDataReceivedCallback",
|
|
634
|
-
0,
|
|
635
|
-
1,
|
|
636
|
-
[this](Napi::Env) {
|
|
637
|
-
// This is the finalizer thread
|
|
638
|
-
this->_nativeThread.join();
|
|
639
|
-
}
|
|
640
|
-
);
|
|
641
|
-
|
|
642
|
-
HandleAudioData();
|
|
558
|
+
_eventListeners[eventName] = Napi::ThreadSafeFunction::New(env, info[1].As<Napi::Function>(), eventName + "Callback", 0, 1);
|
|
643
559
|
}
|
|
644
560
|
}
|
|
645
561
|
|
|
646
|
-
|
|
647
|
-
* Removes an event listener for this room. Requires the event type to remove the listener for.
|
|
648
|
-
* @param info
|
|
649
|
-
* @return
|
|
650
|
-
*/
|
|
651
|
-
void OdinRoom::RemoveEventListener(const Napi::CallbackInfo &info) {
|
|
652
|
-
Napi::Env env = info.Env();
|
|
653
|
-
|
|
654
|
-
if (info.Length() != 1 || !info[0].IsString()) {
|
|
655
|
-
Napi::TypeError::New(env, "Event Name expected").ThrowAsJavaScriptException();
|
|
656
|
-
return;
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
std::string eventName = info[0].As<Napi::String>().Utf8Value();
|
|
660
|
-
|
|
661
|
-
if (eventName != "AudioDataReceived") {
|
|
662
|
-
_eventListeners[eventName].Release();
|
|
663
|
-
_eventListeners.erase(eventName);
|
|
664
|
-
} else {
|
|
665
|
-
// Stop the native thread and this will also release the callback function
|
|
666
|
-
_started = false;
|
|
667
|
-
}
|
|
668
|
-
}
|
|
562
|
+
void OdinRoomWrapper::RemoveEventListener(const Napi::CallbackInfo &info) {}
|
|
669
563
|
|
|
670
564
|
/**
|
|
671
|
-
*
|
|
672
|
-
*
|
|
565
|
+
* Background thread function that processes incoming audio from decoders.
|
|
566
|
+
*
|
|
567
|
+
* Runs continuously while _started is true, popping decoded audio from
|
|
568
|
+
* each decoder and sending it to JavaScript via ThreadSafeFunction callbacks.
|
|
569
|
+
*
|
|
570
|
+
* Safety: Checks _started multiple times per iteration to allow quick exit
|
|
571
|
+
* when Close() is called. Uses NonBlockingCall to avoid deadlocks during shutdown.
|
|
572
|
+
*
|
|
573
|
+
* Ref-counting: Increments _pendingCallbacks before each NonBlockingCall and
|
|
574
|
+
* decrements after the callback executes. Close() waits for this count to reach
|
|
575
|
+
* zero before releasing ThreadSafeFunctions.
|
|
673
576
|
*/
|
|
674
|
-
void
|
|
675
|
-
{
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
#endif
|
|
680
|
-
return;
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
if (_started) {
|
|
684
|
-
#ifdef DEBUG
|
|
685
|
-
printf("Odin NodeJS Addon: Handle Audio Data thread already started\n");
|
|
686
|
-
#endif
|
|
687
|
-
return;
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
_started = true;
|
|
691
|
-
|
|
692
|
-
// Create a native thread
|
|
693
|
-
_nativeThread = std::thread( [this] {
|
|
694
|
-
auto callback = []( Napi::Env env, Napi::Function jsCallback, void* value ) {
|
|
695
|
-
AudioSamples* samples = static_cast<AudioSamples*>( value );
|
|
696
|
-
// Transform native data into JS data, passing it to the provided
|
|
697
|
-
// `jsCallback` -- the TSFN's JavaScript function.
|
|
698
|
-
//Napi::Buffer<float> buffer = Napi::Buffer<float>::New(env, samples->OriginalData, samples->Len);
|
|
699
|
-
//Napi::ArrayBuffer buffer32 = Napi::ArrayBuffer::New(env, samples->OriginalData, samples->Len*sizeof(float));
|
|
700
|
-
//Napi::ArrayBuffer buffer16 = Napi::ArrayBuffer::New(env, samples->Data, samples->Len*sizeof(short));
|
|
701
|
-
Napi::Object obj = Napi::Object::New(env);
|
|
702
|
-
obj.Set("peerId", samples->PeerId);
|
|
703
|
-
obj.Set("mediaId", samples->MediaId);
|
|
704
|
-
obj.Set("samples16", Napi::Buffer<short>::New(env, samples->Data, samples->Len));
|
|
705
|
-
obj.Set("samples32", Napi::Buffer<float>::New(env, samples->OriginalData, samples->Len));
|
|
706
|
-
|
|
707
|
-
jsCallback.Call( {obj} );
|
|
708
|
-
|
|
709
|
-
// We're finished with the data.
|
|
710
|
-
delete samples;
|
|
711
|
-
};
|
|
712
|
-
|
|
713
|
-
while (this->_started)
|
|
577
|
+
void OdinRoomWrapper::HandleAudioData() {
|
|
578
|
+
while (_started) {
|
|
579
|
+
// Early exit check at loop start
|
|
580
|
+
if (!_started) break;
|
|
581
|
+
|
|
714
582
|
{
|
|
715
|
-
|
|
716
|
-
{
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
583
|
+
std::lock_guard<std::mutex> lock(_decodersMutex);
|
|
584
|
+
for (auto& pair : _decoders) {
|
|
585
|
+
// Check again before processing each decoder
|
|
586
|
+
if (!_started) break;
|
|
587
|
+
|
|
588
|
+
OdinDecoder* decoder = pair.second;
|
|
589
|
+
bool silent = false;
|
|
590
|
+
OdinError rc = odin_decoder_pop(decoder, _audioSamplesBuffer, 1920, &silent);
|
|
591
|
+
if (!odin_is_error(rc) && !silent) {
|
|
592
|
+
// Double-check _started before calling callback
|
|
593
|
+
// This prevents calling a released ThreadSafeFunction
|
|
594
|
+
if (_started && _audioDataReceivedEventListener) {
|
|
595
|
+
AudioSamples* samples = new AudioSamples();
|
|
596
|
+
samples->MediaId = pair.first;
|
|
597
|
+
// Look up peer ID from media-to-peer mapping
|
|
598
|
+
{
|
|
599
|
+
std::lock_guard<std::mutex> peerLock(_mediaToPeerMutex);
|
|
600
|
+
auto peerIt = _mediaToPeer.find(pair.first);
|
|
601
|
+
samples->PeerId = (peerIt != _mediaToPeer.end()) ? peerIt->second : 0;
|
|
602
|
+
}
|
|
603
|
+
samples->SetSamples(_audioSamplesBuffer, 1920);
|
|
604
|
+
|
|
605
|
+
// Store pointer to pending counter in samples for callback to decrement
|
|
606
|
+
samples->PendingCounterPtr = &_pendingCallbacks;
|
|
607
|
+
|
|
608
|
+
// Increment pending count BEFORE NonBlockingCall
|
|
609
|
+
_pendingCallbacks.fetch_add(1);
|
|
610
|
+
|
|
611
|
+
// Callback that decrements counter after execution
|
|
612
|
+
// NOTE: Adding try-catch and logging to diagnose crashes
|
|
613
|
+
auto callback = [](Napi::Env env, Napi::Function jsCallback, void* value) {
|
|
614
|
+
AudioSamples* samples = (AudioSamples*)value;
|
|
615
|
+
|
|
616
|
+
// Get the counter pointer before we delete samples
|
|
617
|
+
std::atomic<int>* pendingPtr = samples->PendingCounterPtr;
|
|
618
|
+
|
|
619
|
+
try {
|
|
620
|
+
// Execute callback
|
|
621
|
+
Napi::Object obj = Napi::Object::New(env);
|
|
622
|
+
obj.Set("mediaId", samples->MediaId);
|
|
623
|
+
obj.Set("peerId", (double)samples->PeerId);
|
|
624
|
+
obj.Set("samples16", Napi::Buffer<short>::New(env, samples->Data, samples->Len));
|
|
625
|
+
obj.Set("samples32", Napi::Buffer<float>::New(env, samples->OriginalData, samples->Len));
|
|
626
|
+
jsCallback.Call({obj});
|
|
627
|
+
} catch (...) {
|
|
628
|
+
// Exception in audio callback - silently ignore
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
delete samples;
|
|
632
|
+
|
|
633
|
+
// Decrement pending count AFTER callback completes
|
|
634
|
+
if (pendingPtr) {
|
|
635
|
+
pendingPtr->fetch_sub(1);
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
// Use NonBlockingCall to avoid potential deadlocks during shutdown
|
|
640
|
+
_audioDataReceivedEventListener.NonBlockingCall(samples, callback);
|
|
641
|
+
}
|
|
736
642
|
}
|
|
737
643
|
}
|
|
738
|
-
|
|
739
|
-
std::this_thread::sleep_for( std::chrono::milliseconds ( 20 ) );
|
|
740
644
|
}
|
|
645
|
+
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
|
646
|
+
}
|
|
647
|
+
}
|
|
741
648
|
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
649
|
+
Napi::Value OdinRoomWrapper::CreateAudioStream(const Napi::CallbackInfo &info) {
|
|
650
|
+
std::vector<napi_value> args;
|
|
651
|
+
args.push_back(this->Value());
|
|
652
|
+
for (size_t i = 0; i < info.Length(); i++) args.push_back(info[i]);
|
|
653
|
+
return OdinMediaWrapper::NewInstance(args);
|
|
654
|
+
}
|
|
745
655
|
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
656
|
+
void OdinRoomWrapper::SetCipher(const Napi::CallbackInfo &info) {
|
|
657
|
+
Napi::Env env = info.Env();
|
|
658
|
+
if (info.Length() < 1 || !info[0].IsObject()) {
|
|
659
|
+
Napi::TypeError::New(env, "OdinCipher instance expected").ThrowAsJavaScriptException();
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
OdinCipherWrapper* cipherWrapper = Napi::ObjectWrap<OdinCipherWrapper>::Unwrap(info[0].As<Napi::Object>());
|
|
663
|
+
_cipher = cipherWrapper->GetCipher();
|
|
664
|
+
|
|
665
|
+
// Transfer ownership: the room now owns the cipher and will free it.
|
|
666
|
+
// The wrapper must not try to free it again in its destructor.
|
|
667
|
+
cipherWrapper->TransferOwnership();
|
|
750
668
|
}
|
|
751
669
|
|
|
752
|
-
Napi::Value
|
|
753
|
-
|
|
754
|
-
|
|
670
|
+
Napi::Value OdinRoomWrapper::GetOwnPeerId(const Napi::CallbackInfo &info) {
|
|
671
|
+
return Napi::Number::New(info.Env(), static_cast<double>(_ownPeerId));
|
|
672
|
+
}
|
|
755
673
|
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
674
|
+
void OdinRoomWrapper::SetOwnPeerId(const Napi::CallbackInfo &info) {
|
|
675
|
+
Napi::Env env = info.Env();
|
|
676
|
+
if (info.Length() < 1 || !info[0].IsNumber()) {
|
|
677
|
+
Napi::TypeError::New(env, "Peer ID (number) required").ThrowAsJavaScriptException();
|
|
678
|
+
return;
|
|
760
679
|
}
|
|
680
|
+
_ownPeerId = static_cast<uint64_t>(info[0].As<Napi::Number>().DoubleValue());
|
|
681
|
+
}
|
|
761
682
|
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
683
|
+
void OdinRoomWrapper::RegisterMediaPeer(const Napi::CallbackInfo &info) {
|
|
684
|
+
Napi::Env env = info.Env();
|
|
685
|
+
if (info.Length() < 2 || !info[0].IsNumber() || !info[1].IsNumber()) {
|
|
686
|
+
Napi::TypeError::New(env, "Media ID and Peer ID (numbers) required").ThrowAsJavaScriptException();
|
|
687
|
+
return;
|
|
765
688
|
}
|
|
689
|
+
uint16_t mediaId = static_cast<uint16_t>(info[0].As<Napi::Number>().Uint32Value());
|
|
690
|
+
uint64_t peerId = static_cast<uint64_t>(info[1].As<Napi::Number>().DoubleValue());
|
|
691
|
+
|
|
692
|
+
std::lock_guard<std::mutex> lock(_mediaToPeerMutex);
|
|
693
|
+
_mediaToPeer[mediaId] = peerId;
|
|
694
|
+
}
|
|
766
695
|
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
696
|
+
void OdinRoomWrapper::UnregisterMedia(const Napi::CallbackInfo &info) {
|
|
697
|
+
Napi::Env env = info.Env();
|
|
698
|
+
if (info.Length() < 1 || !info[0].IsNumber()) {
|
|
699
|
+
Napi::TypeError::New(env, "Media ID (number) required").ThrowAsJavaScriptException();
|
|
700
|
+
return;
|
|
771
701
|
}
|
|
702
|
+
uint16_t mediaId = static_cast<uint16_t>(info[0].As<Napi::Number>().Uint32Value());
|
|
703
|
+
|
|
704
|
+
std::lock_guard<std::mutex> lock(_mediaToPeerMutex);
|
|
705
|
+
_mediaToPeer.erase(mediaId);
|
|
772
706
|
}
|
|
773
707
|
|
|
774
|
-
|
|
775
708
|
/**
|
|
776
|
-
*
|
|
777
|
-
*
|
|
778
|
-
*
|
|
709
|
+
* Retrieves detailed connection statistics for the room.
|
|
710
|
+
* Returns an object containing UDP transmission/reception stats, packet loss, RTT, etc.
|
|
711
|
+
*
|
|
712
|
+
* @returns {Object} Connection statistics:
|
|
713
|
+
* - udpTxDatagrams: Number of outgoing UDP datagrams
|
|
714
|
+
* - udpTxBytes: Total bytes sent
|
|
715
|
+
* - udpTxLoss: Packet loss percentage for outgoing datagrams
|
|
716
|
+
* - udpRxDatagrams: Number of incoming UDP datagrams
|
|
717
|
+
* - udpRxBytes: Total bytes received
|
|
718
|
+
* - udpRxLoss: Packet loss percentage for incoming datagrams
|
|
719
|
+
* - cwnd: Current congestion window size
|
|
720
|
+
* - congestionEvents: Number of congestion events
|
|
721
|
+
* - rtt: Round-trip time in milliseconds
|
|
779
722
|
*/
|
|
780
|
-
Napi::Value
|
|
723
|
+
Napi::Value OdinRoomWrapper::GetConnectionStats(const Napi::CallbackInfo &info) {
|
|
781
724
|
Napi::Env env = info.Env();
|
|
725
|
+
|
|
726
|
+
if (!_room || !_joined) {
|
|
727
|
+
return env.Null();
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
OdinConnectionStats stats;
|
|
731
|
+
memset(&stats, 0, sizeof(stats));
|
|
732
|
+
|
|
733
|
+
OdinError rc = odin_room_get_connection_stats(_room, &stats);
|
|
734
|
+
if (odin_is_error(rc)) {
|
|
735
|
+
OdinUtilities::ThrowNapiException(env, rc, "Failed to get connection stats");
|
|
736
|
+
return env.Null();
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// Build the JavaScript object with all stats
|
|
740
|
+
Napi::Object result = Napi::Object::New(env);
|
|
741
|
+
result.Set("udpTxDatagrams", Napi::Number::New(env, static_cast<double>(stats.udp_tx_datagrams)));
|
|
742
|
+
result.Set("udpTxBytes", Napi::Number::New(env, static_cast<double>(stats.udp_tx_bytes)));
|
|
743
|
+
result.Set("udpTxLoss", Napi::Number::New(env, stats.udp_tx_loss));
|
|
744
|
+
result.Set("udpRxDatagrams", Napi::Number::New(env, static_cast<double>(stats.udp_rx_datagrams)));
|
|
745
|
+
result.Set("udpRxBytes", Napi::Number::New(env, static_cast<double>(stats.udp_rx_bytes)));
|
|
746
|
+
result.Set("udpRxLoss", Napi::Number::New(env, stats.udp_rx_loss));
|
|
747
|
+
result.Set("cwnd", Napi::Number::New(env, static_cast<double>(stats.cwnd)));
|
|
748
|
+
result.Set("congestionEvents", Napi::Number::New(env, static_cast<double>(stats.congestion_events)));
|
|
749
|
+
result.Set("rtt", Napi::Number::New(env, stats.rtt));
|
|
750
|
+
|
|
751
|
+
return result;
|
|
752
|
+
}
|
|
782
753
|
|
|
783
|
-
|
|
784
|
-
|
|
754
|
+
/**
|
|
755
|
+
* Retrieves the underlying connection identifier for the room.
|
|
756
|
+
* Returns 0 if no valid connection exists.
|
|
757
|
+
*
|
|
758
|
+
* @returns {number} The connection ID, or 0 if not connected
|
|
759
|
+
*/
|
|
760
|
+
Napi::Value OdinRoomWrapper::GetConnectionId(const Napi::CallbackInfo &info) {
|
|
761
|
+
Napi::Env env = info.Env();
|
|
762
|
+
|
|
763
|
+
if (!_room) {
|
|
764
|
+
return Napi::Number::New(env, 0);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
uint64_t connectionId = odin_room_get_connection_id(_room);
|
|
768
|
+
return Napi::Number::New(env, static_cast<double>(connectionId));
|
|
769
|
+
}
|
|
785
770
|
|
|
786
|
-
|
|
771
|
+
/**
|
|
772
|
+
* Retrieves jitter buffer statistics for a specific decoder (media stream).
|
|
773
|
+
* Useful for debugging audio quality issues.
|
|
774
|
+
*
|
|
775
|
+
* @param info[0] Media ID (number) - The media stream to get stats for
|
|
776
|
+
* @returns {Object|null} Jitter statistics, or null if decoder not found:
|
|
777
|
+
* - packetsTotal: Total packets seen by jitter buffer
|
|
778
|
+
* - packetsBuffered: Packets currently in buffer
|
|
779
|
+
* - packetsProcessed: Packets successfully processed
|
|
780
|
+
* - packetsArrivedTooEarly: Packets dropped (arrived too early)
|
|
781
|
+
* - packetsArrivedTooLate: Packets dropped (arrived too late)
|
|
782
|
+
* - packetsDropped: Packets dropped due to buffer reset
|
|
783
|
+
* - packetsInvalid: Invalid packets received
|
|
784
|
+
* - packetsRepeated: Duplicate packets received
|
|
785
|
+
* - packetsLost: Packets lost during transmission
|
|
786
|
+
*/
|
|
787
|
+
Napi::Value OdinRoomWrapper::GetJitterStats(const Napi::CallbackInfo &info) {
|
|
788
|
+
Napi::Env env = info.Env();
|
|
789
|
+
|
|
790
|
+
if (info.Length() < 1 || !info[0].IsNumber()) {
|
|
791
|
+
Napi::TypeError::New(env, "Media ID (number) required").ThrowAsJavaScriptException();
|
|
792
|
+
return env.Null();
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
uint16_t mediaId = static_cast<uint16_t>(info[0].As<Napi::Number>().Uint32Value());
|
|
796
|
+
|
|
797
|
+
// Find the decoder for this media ID
|
|
798
|
+
std::lock_guard<std::mutex> lock(_decodersMutex);
|
|
799
|
+
auto it = _decoders.find(mediaId);
|
|
800
|
+
if (it == _decoders.end()) {
|
|
801
|
+
// No decoder for this media ID yet - return null
|
|
802
|
+
return env.Null();
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
OdinDecoderJitterStats stats;
|
|
806
|
+
memset(&stats, 0, sizeof(stats));
|
|
807
|
+
|
|
808
|
+
OdinError rc = odin_decoder_get_jitter_stats(it->second, &stats);
|
|
809
|
+
if (odin_is_error(rc)) {
|
|
810
|
+
OdinUtilities::ThrowNapiException(env, rc, "Failed to get jitter stats");
|
|
811
|
+
return env.Null();
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// Build the JavaScript object with all jitter stats
|
|
815
|
+
Napi::Object result = Napi::Object::New(env);
|
|
816
|
+
result.Set("packetsTotal", Napi::Number::New(env, stats.packets_total));
|
|
817
|
+
result.Set("packetsBuffered", Napi::Number::New(env, stats.packets_buffered));
|
|
818
|
+
result.Set("packetsProcessed", Napi::Number::New(env, stats.packets_processed));
|
|
819
|
+
result.Set("packetsArrivedTooEarly", Napi::Number::New(env, stats.packets_arrived_too_early));
|
|
820
|
+
result.Set("packetsArrivedTooLate", Napi::Number::New(env, stats.packets_arrived_too_late));
|
|
821
|
+
result.Set("packetsDropped", Napi::Number::New(env, stats.packets_dropped));
|
|
822
|
+
result.Set("packetsInvalid", Napi::Number::New(env, stats.packets_invalid));
|
|
823
|
+
result.Set("packetsRepeated", Napi::Number::New(env, stats.packets_repeated));
|
|
824
|
+
result.Set("packetsLost", Napi::Number::New(env, stats.packets_lost));
|
|
825
|
+
|
|
826
|
+
return result;
|
|
787
827
|
}
|