@4players/odin-nodejs 0.10.3 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/LICENSE +21 -0
  3. package/README.md +603 -44
  4. package/binding.gyp +29 -13
  5. package/cppsrc/binding.cpp +3 -6
  6. package/cppsrc/odinbindings.cpp +9 -45
  7. package/cppsrc/odincipher.cpp +92 -0
  8. package/cppsrc/odincipher.h +32 -0
  9. package/cppsrc/odinclient.cpp +19 -158
  10. package/cppsrc/odinclient.h +2 -5
  11. package/cppsrc/odinmedia.cpp +144 -186
  12. package/cppsrc/odinmedia.h +51 -18
  13. package/cppsrc/odinroom.cpp +675 -635
  14. package/cppsrc/odinroom.h +76 -26
  15. package/cppsrc/utilities.cpp +11 -81
  16. package/cppsrc/utilities.h +25 -140
  17. package/index.cjs +829 -0
  18. package/index.d.ts +3 -4
  19. package/libs/bin/linux/arm64/libodin.so +0 -0
  20. package/libs/bin/linux/arm64/libodin_crypto.so +0 -0
  21. package/libs/bin/linux/ia32/libodin.so +0 -0
  22. package/libs/bin/linux/ia32/libodin_crypto.so +0 -0
  23. package/libs/bin/linux/x64/libodin.so +0 -0
  24. package/libs/bin/linux/x64/libodin_crypto.so +0 -0
  25. package/{prebuilds/darwin-x64/node.napi.node → libs/bin/macos/universal/libodin.dylib} +0 -0
  26. package/libs/bin/macos/universal/libodin_crypto.dylib +0 -0
  27. package/libs/bin/windows/arm64/odin.dll +0 -0
  28. package/libs/bin/windows/arm64/odin.lib +0 -0
  29. package/libs/bin/windows/arm64/odin_crypto.dll +0 -0
  30. package/libs/bin/windows/arm64/odin_crypto.lib +0 -0
  31. package/libs/bin/windows/ia32/odin.dll +0 -0
  32. package/libs/bin/windows/ia32/odin.lib +0 -0
  33. package/libs/bin/windows/ia32/odin_crypto.dll +0 -0
  34. package/libs/bin/windows/ia32/odin_crypto.lib +0 -0
  35. package/libs/bin/windows/x64/odin.dll +0 -0
  36. package/libs/bin/windows/x64/odin.lib +0 -0
  37. package/libs/bin/windows/x64/odin_crypto.dll +0 -0
  38. package/libs/bin/windows/x64/odin_crypto.lib +0 -0
  39. package/libs/include/odin.h +665 -567
  40. package/libs/include/odin_crypto.h +46 -0
  41. package/odin.cipher.d.ts +31 -0
  42. package/odin.media.d.ts +69 -19
  43. package/odin.room.d.ts +348 -7
  44. package/package.json +5 -4
  45. package/prebuilds/{darwin-arm64/node.napi.node → darwin-x64+arm64/libodin.dylib} +0 -0
  46. package/prebuilds/darwin-x64+arm64/libodin_crypto.dylib +0 -0
  47. package/prebuilds/darwin-x64+arm64/node.napi.node +0 -0
  48. package/prebuilds/linux-x64/libodin.so +0 -0
  49. package/prebuilds/linux-x64/libodin_crypto.so +0 -0
  50. package/prebuilds/linux-x64/node.napi.node +0 -0
  51. package/prebuilds/win32-x64/node.napi.node +0 -0
  52. package/prebuilds/win32-x64/odin.dll +0 -0
  53. package/prebuilds/win32-x64/odin_crypto.dll +0 -0
  54. package/scripts/postbuild.cjs +133 -0
  55. package/tests/audio-recording/README.md +97 -12
  56. package/tests/audio-recording/index.js +238 -130
  57. package/tests/connection-test/README.md +97 -0
  58. package/tests/connection-test/index.js +273 -0
  59. package/tests/lifecycle/test-room-cycle.js +169 -0
  60. package/tests/sending-audio/README.md +178 -9
  61. package/tests/sending-audio/canBounce.mp3 +0 -0
  62. package/tests/sending-audio/index.js +250 -87
  63. package/tests/sending-audio/test-kiss-api.js +149 -0
  64. package/tests/sending-audio/test-loop-audio.js +142 -0
  65. package/CMakeLists.txt +0 -25
  66. package/libs/bin/linux/arm64/libodin_static.a +0 -0
  67. package/libs/bin/linux/ia32/libodin_static.a +0 -0
  68. package/libs/bin/linux/x64/libodin_static.a +0 -0
  69. package/libs/bin/macos/arm64/libodin_static.a +0 -0
  70. package/libs/bin/macos/x64/libodin_static.a +0 -0
  71. package/libs/bin/windows/arm64/odin_static.lib +0 -0
  72. package/libs/bin/windows/ia32/odin_static.lib +0 -0
  73. package/libs/bin/windows/x64/odin_static.lib +0 -0
@@ -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* OdinRoom::constructor;
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
- * Prepares a NAPI value object for the event
19
- * @param env
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
- Napi::Object OdinRoom::PrepareEventObject(Napi::Env env, const EventData *event) {
24
- Napi::Object obj = Napi::Object::New(env);
25
- obj.Set("event", event->Event);
26
- obj.Set("tag", (int)event->Tag);
27
- switch(event->Tag) {
28
- case OdinEvent_Joined: {
29
- const JoinedEventData *joined = static_cast<const JoinedEventData*>(event);
30
- obj.Set("roomId", joined->RoomId);
31
- obj.Set("ownPeerId", joined->PeerId);
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
- * Returns the room id as string of this room instance
107
- * @param info
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
- Napi::Value OdinRoom::RoomId(const Napi::CallbackInfo &info) {
110
- Napi::Env env = info.Env();
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
- char out_id[512];
113
- OdinReturnCode error = odin_room_id(_roomHandle, out_id, sizeof(out_id));
114
- if (odin_is_error(error))
115
- {
116
- Napi::TypeError::New(env, "Failed to get room id").ThrowAsJavaScriptException();
117
- return env.Undefined();
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
- return Napi::String::New(env, out_id);
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
- * Returns the room handle of this room instance
125
- */
126
- OdinRoomHandle OdinRoom::GetRoomHandle() const {
127
- return _roomHandle;
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
- * Returns the media id of a media stream handle, used internally to map media stream handles to media ids in event callbacks
132
- */
133
- uint16_t OdinRoom::GetMediaIdFromHandle(OdinMediaStreamHandle handle)
134
- {
135
- uint16_t media_id;
136
- int error = odin_media_stream_media_id(handle, &media_id);
137
- return odin_is_error(error) ? 0 : media_id;
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
- * Creates an EventData object instance from an OdinEvent. As OdinEvents are allocated on the Odin SDK side, we need to
142
- * copy the data as the callbacks to the JS side are asynchronous.
143
- * @param event
144
- * @return EventData
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
- EventData* OdinRoom::PrepareEventData(OdinEvent* event)
147
- {
148
- switch(event->tag) {
149
- case OdinEvent_Joined: {
150
- JoinedEventData* data = new JoinedEventData();
151
- data->Tag = event->tag;
152
- data->Event = "Joined";
153
- data->RoomId = event->joined.room_id;
154
- data->UserId = event->joined.own_user_id;
155
- data->PeerId = event->joined.own_peer_id;
156
- if (event->joined.room_user_data && event->joined.room_user_data_len > 0) {
157
- data->SetData(event->joined.room_user_data, event->joined.room_user_data_len);
158
- }
159
- return data;
160
- }
161
- case OdinEvent_PeerJoined: {
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
- return NULL;
142
+
143
+ if (it != _decoders.end()) {
144
+ odin_decoder_push(it->second, bytes, bytes_length);
145
+ }
249
146
  }
250
147
 
251
148
  /**
252
- * Callback function for Odin events. This function is called by the Odin SDK when an event occurs. It creates an EventData
253
- * object instance from the OdinEvent and calls the JavaScript callback function with the event data.
254
- * @param room OdinRoomHandle
255
- * @param event OdinEvent
256
- * @param data void* Pointer to the OdinRoom instance
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 OdinRoom::HandleOdinEvent(OdinRoomHandle room, const OdinEvent *event, void *data)
259
- {
260
- OdinRoom* odinRoom = (OdinRoom*)data;
261
- if (odinRoom != NULL) {
262
-
263
- ::uint16_t mediaId = OdinRoom::GetMediaIdFromHandle(event->media_added.media_handle);
264
- if (event->tag == OdinEvent_MediaAdded) {
265
- Media media;
266
- media.PeerId = event->media_added.peer_id;
267
- media.Id = mediaId;
268
- media.Handle = event->media_added.media_handle;
269
- odinRoom->_mediaStreams[media.Id] = media;
270
- } else if (event->tag == OdinEvent_MediaRemoved) {
271
- if (odinRoom->_mediaStreams.find(mediaId) != odinRoom->_mediaStreams.end()) {
272
- odinRoom->_mediaStreams.erase(mediaId);
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
- auto callback = []( Napi::Env env, Napi::Function jsCallback, void* value ) {
277
- EventData* eventData = (EventData*)value;
278
- if (eventData == NULL) {
279
- // I have no idea how to call a function without a parameter ;-)
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
- EventData* data = odinRoom->PrepareEventData((OdinEvent*)event);
298
- Napi::ThreadSafeFunction function = odinRoom->_eventListeners[data->Event];
299
- if (function != NULL) {
300
- function.BlockingCall( (void*)data, callback );
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
- _roomHandle = odin_room_create();
322
- _started = false;
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
- * Destructor. Closes and destroys the OdinRoom and resets the event callback for this room handle.
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 OdinRoom::Finalize(Napi::Env env) {
332
- odin_room_set_event_callback(_roomHandle, NULL, NULL);
333
- odin_room_close(_roomHandle);
334
- odin_room_destroy(_roomHandle);
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<&OdinRoom::Join>("join", static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
345
- InstanceMethod<&OdinRoom::UpdatePeerUserData>("updateOwnUserData", static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
346
- InstanceMethod<&OdinRoom::UpdatePosition>("updatePosition", static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
347
- InstanceMethod<&OdinRoom::SetPositionScale>("setPositionScale", static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
348
- InstanceMethod<&OdinRoom::SendMessage>("sendMessage", static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
349
- InstanceMethod<&OdinRoom::SetEventListener>("setEventListener", static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
350
- InstanceMethod<&OdinRoom::AddEventListener>("addEventListener", static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
351
- InstanceMethod<&OdinRoom::RemoveEventListener>("removeEventListener", static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
352
- InstanceMethod<&OdinRoom::Close>("close", static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
353
- InstanceMethod<&OdinRoom::CreateAudioStream>("createAudioStream", static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
354
- InstanceAccessor("ownPeerId", &OdinRoom::GetOwnPeerId, nullptr, static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
355
- InstanceAccessor("id", &OdinRoom::RoomId, nullptr, static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
356
- StaticMethod<&OdinRoom::CreateNewItem>("CreateNewItem", static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
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 this room. Requires x, y and z coordinates as parameters.
384
- * @param info Napi::CallbackInfo
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 OdinRoom::UpdatePosition(const Napi::CallbackInfo &info)
387
- {
322
+ void OdinRoomWrapper::UpdatePosition(const Napi::CallbackInfo &info) {
388
323
  Napi::Env env = info.Env();
389
-
390
- if (info.Length() != 3 || !info[0].IsNumber() || !info[1].IsNumber() || !info[2].IsNumber()) {
391
- Napi::TypeError::New(env, "Provide x, y and z coordinates as numbers").ThrowAsJavaScriptException();
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
- float x = info[0].ToNumber();
396
- float y = info[1].ToNumber();
397
- float z = info[2].ToNumber();
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 position scale for this room. Requires a scale as a parameter.
408
- * @param info Napi::CallbackInfo
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 OdinRoom::SetPositionScale(const Napi::CallbackInfo &info)
411
- {
352
+ void OdinRoomWrapper::SetPositionScale(const Napi::CallbackInfo &info) {
412
353
  Napi::Env env = info.Env();
413
-
414
- if (info.Length() != 1 || !info[0].IsNumber()) {
415
- Napi::TypeError::New(env, "Provide scale as number").ThrowAsJavaScriptException();
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
- float scale = info[0].ToNumber();
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 as first parameter").ThrowAsJavaScriptException();
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
- if (info.Length() > 1) {
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
- OdinReturnCode error = odin_room_update_peer_user_data(_roomHandle, data.Data(), data.ByteLength());
445
- if (odin_is_error(error))
446
- {
447
- Napi::TypeError::New(env, "Setting initial room peer data failed").ThrowAsJavaScriptException();
448
- }
449
- }
450
-
451
- OdinReturnCode error = odin_room_join(_roomHandle, url.c_str(), _token.c_str());
452
- if (odin_is_error(error))
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
- _joined = true;
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 destroys the room handle.
468
- * @param info
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 OdinRoom::Close(const Napi::CallbackInfo &info) {
471
- //Napi::Env env = info.Env();
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
- for (auto it = _eventListeners.begin(); it != _eventListeners.end(); ++it) {
483
- if (it->second != NULL) {
484
- it->second.Release();
485
- it->second = NULL;
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
- odin_room_close(_roomHandle);
490
- odin_room_destroy(_roomHandle);
491
- _roomHandle = 0;
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
- Napi::String token = info[0].ToString();
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
- if (odin_is_error(error))
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 message to the room. Requires an array of bytes as a parameter and optinally a list of peer ids to send to.
551
- * Please note: If the list is NULL the message will be sent to all peers in the room. If the list is not NULL the message
552
- * will be sent to all peers in the list. If the list is empty the message ODIN will raise an error.
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 OdinRoom::SendMessage(const Napi::CallbackInfo &info) {
518
+ void OdinRoomWrapper::SendRpc(const Napi::CallbackInfo &info) {
557
519
  Napi::Env env = info.Env();
558
-
559
- if (info.Length() < 1 || !info[0].IsTypedArray()) {
560
- Napi::TypeError::New(env, "Data as byte array expected").ThrowAsJavaScriptException();
520
+
521
+ if (!_room || !_joined) {
522
+ Napi::Error::New(env, "Room not joined").ThrowAsJavaScriptException();
561
523
  return;
562
524
  }
563
-
564
- Napi::Uint8Array data = info[0].As<Napi::Uint8Array>();
565
-
566
- uint64_t *peer_id_list = NULL;
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
- if (odin_is_error(error))
588
- {
589
- OdinUtilities::ThrowNapiException(env, error, "Failed to send message");
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 function expected").ThrowAsJavaScriptException();
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
- Napi::Function napiFunction = info[1].As<Napi::Function>();
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
- _audioDataReceivedEventListener = Napi::ThreadSafeFunction::New(
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
- * Internal function to receive audio data every 20ms and send it to the JS side. This function will start once the client
672
- * has registered AudioDataReceived as an event listener.
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 OdinRoom::HandleAudioData()
675
- {
676
- if (!_joined) {
677
- #ifdef DEBUG
678
- printf("Odin NodeJS Addon: Skipping audio until joined\n");
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
- for (auto it = _mediaStreams.begin(); it != _mediaStreams.end(); it++)
716
- {
717
- OdinReturnCode rc = odin_audio_read_data((OdinMediaStreamHandle)it->second.Handle, this->_audioSamplesBuffer, 960);
718
-
719
- if (odin_is_error(rc)) {
720
- printf("Odin NodeJS Addon: Failed to read audio data\n");
721
- break;
722
- }
723
-
724
- AudioSamples* samples = new AudioSamples();
725
- samples->SetSamples(this->_audioSamplesBuffer, 960);
726
- samples->PeerId = it->second.PeerId;
727
- samples->MediaId = it->second.Id;
728
-
729
- // Perform a blocking call
730
- napi_status status = _audioDataReceivedEventListener.BlockingCall( samples, callback );
731
- if ( status != napi_ok )
732
- {
733
- // Handle error
734
- printf("Odin NodeJS Addon: Failed to call audio data received callback\n");
735
- break;
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
- #ifdef DEBUG
743
- printf("Odin NodeJS Addon: Handle Audio Thread finished.");
744
- #endif
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
- // Release the thread-safe function
747
- _audioDataReceivedEventListener.Release();
748
- _audioDataReceivedEventListener = NULL;
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 OdinRoom::CreateAudioStream(const Napi::CallbackInfo &info)
753
- {
754
- Napi::Env env = info.Env();
670
+ Napi::Value OdinRoomWrapper::GetOwnPeerId(const Napi::CallbackInfo &info) {
671
+ return Napi::Number::New(info.Env(), static_cast<double>(_ownPeerId));
672
+ }
755
673
 
756
- // Checking for input of sample rate (int) and number of channels (int)
757
- if (info.Length() != 2 || !info[0].IsNumber() || !info[1].IsNumber()) {
758
- Napi::TypeError::New(env, "Sample rate and number of channels expected").ThrowAsJavaScriptException();
759
- return env.Undefined();
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
- if (info.Length() == 3 && !info[2].IsObject()) {
763
- Napi::TypeError::New(env, "Options need to be an object").ThrowAsJavaScriptException();
764
- return env.Undefined();
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
- if (info.Length() == 2) {
768
- return OdinMedia::NewInstance({this->Value(), info[0], info[1]});
769
- } else {
770
- return OdinMedia::NewInstance({this->Value(), info[0], info[1], info[2]});
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
- * Removes an event listener for this room. Requires the event type to remove the listener for.
777
- * @param info
778
- * @return
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 OdinRoom::GetOwnPeerId(const Napi::CallbackInfo &info) {
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
- uint64_t out_peer_id = 0;
784
- odin_room_peer_id(this->_roomHandle, &out_peer_id);
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
- return Napi::Number::New(env, out_peer_id);
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
  }