@mertushka/webrtc-node 0.1.0-alpha.0
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/CMakeLists.txt +143 -0
- package/LICENSE +373 -0
- package/README.md +166 -0
- package/docs/README.md +14 -0
- package/docs/architecture.md +38 -0
- package/docs/conformance.md +53 -0
- package/docs/development.md +161 -0
- package/docs/divergences.md +230 -0
- package/examples/datachannel.js +57 -0
- package/index.d.ts +340 -0
- package/lib/index.js +4139 -0
- package/lib/load-native.js +42 -0
- package/package.json +94 -0
- package/scripts/check-api-surface.js +149 -0
- package/scripts/check-ci-evidence.js +124 -0
- package/scripts/check-native-integration.js +124 -0
- package/scripts/check-package-artifact.js +91 -0
- package/scripts/check-prebuilds.js +31 -0
- package/scripts/check-wpt-results.js +117 -0
- package/scripts/check-wpt-selection.js +72 -0
- package/scripts/ensure-wpt.js +118 -0
- package/scripts/install-native.js +116 -0
- package/scripts/package-prebuild.js +69 -0
- package/scripts/print-wpt-manifest.js +7 -0
- package/scripts/run-docker-linux-ci.ps1 +73 -0
- package/scripts/run-docker-linux-ci.sh +97 -0
- package/scripts/run-wpt-smoke.js +32 -0
- package/scripts/run-wpt-subset.js +1193 -0
- package/scripts/write-ci-evidence.js +118 -0
- package/scripts/write-wpt-report.js +143 -0
- package/src/native/addon.cc +1202 -0
- package/wpt-manifest.json +129 -0
|
@@ -0,0 +1,1202 @@
|
|
|
1
|
+
#include <napi.h>
|
|
2
|
+
#include <rtc/rtc.hpp>
|
|
3
|
+
|
|
4
|
+
#include <atomic>
|
|
5
|
+
#include <chrono>
|
|
6
|
+
#include <cstddef>
|
|
7
|
+
#include <cstring>
|
|
8
|
+
#include <cmath>
|
|
9
|
+
#include <iomanip>
|
|
10
|
+
#include <memory>
|
|
11
|
+
#include <mutex>
|
|
12
|
+
#include <optional>
|
|
13
|
+
#include <sstream>
|
|
14
|
+
#include <stdexcept>
|
|
15
|
+
#include <string>
|
|
16
|
+
#include <thread>
|
|
17
|
+
#include <unordered_map>
|
|
18
|
+
#include <utility>
|
|
19
|
+
#include <vector>
|
|
20
|
+
|
|
21
|
+
#include <openssl/bio.h>
|
|
22
|
+
#include <openssl/err.h>
|
|
23
|
+
#include <openssl/evp.h>
|
|
24
|
+
#include <openssl/ec.h>
|
|
25
|
+
#include <openssl/pem.h>
|
|
26
|
+
#include <openssl/rsa.h>
|
|
27
|
+
#include <openssl/x509.h>
|
|
28
|
+
|
|
29
|
+
namespace {
|
|
30
|
+
|
|
31
|
+
class NativeDataChannel;
|
|
32
|
+
struct ChannelBinding;
|
|
33
|
+
struct EventDispatcher;
|
|
34
|
+
struct PeerBinding;
|
|
35
|
+
|
|
36
|
+
std::atomic<int> nextChannelId{1};
|
|
37
|
+
|
|
38
|
+
std::string ToString(rtc::PeerConnection::State state) {
|
|
39
|
+
switch (state) {
|
|
40
|
+
case rtc::PeerConnection::State::New:
|
|
41
|
+
return "new";
|
|
42
|
+
case rtc::PeerConnection::State::Connecting:
|
|
43
|
+
return "connecting";
|
|
44
|
+
case rtc::PeerConnection::State::Connected:
|
|
45
|
+
return "connected";
|
|
46
|
+
case rtc::PeerConnection::State::Disconnected:
|
|
47
|
+
return "disconnected";
|
|
48
|
+
case rtc::PeerConnection::State::Failed:
|
|
49
|
+
return "failed";
|
|
50
|
+
case rtc::PeerConnection::State::Closed:
|
|
51
|
+
return "closed";
|
|
52
|
+
}
|
|
53
|
+
return "closed";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
std::string ToString(rtc::PeerConnection::IceState state) {
|
|
57
|
+
switch (state) {
|
|
58
|
+
case rtc::PeerConnection::IceState::New:
|
|
59
|
+
return "new";
|
|
60
|
+
case rtc::PeerConnection::IceState::Checking:
|
|
61
|
+
return "checking";
|
|
62
|
+
case rtc::PeerConnection::IceState::Connected:
|
|
63
|
+
return "connected";
|
|
64
|
+
case rtc::PeerConnection::IceState::Completed:
|
|
65
|
+
return "completed";
|
|
66
|
+
case rtc::PeerConnection::IceState::Failed:
|
|
67
|
+
return "failed";
|
|
68
|
+
case rtc::PeerConnection::IceState::Disconnected:
|
|
69
|
+
return "disconnected";
|
|
70
|
+
case rtc::PeerConnection::IceState::Closed:
|
|
71
|
+
return "closed";
|
|
72
|
+
}
|
|
73
|
+
return "closed";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
std::string ToString(rtc::PeerConnection::GatheringState state) {
|
|
77
|
+
switch (state) {
|
|
78
|
+
case rtc::PeerConnection::GatheringState::New:
|
|
79
|
+
return "new";
|
|
80
|
+
case rtc::PeerConnection::GatheringState::InProgress:
|
|
81
|
+
return "gathering";
|
|
82
|
+
case rtc::PeerConnection::GatheringState::Complete:
|
|
83
|
+
return "complete";
|
|
84
|
+
}
|
|
85
|
+
return "complete";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
std::string ToString(rtc::PeerConnection::SignalingState state) {
|
|
89
|
+
switch (state) {
|
|
90
|
+
case rtc::PeerConnection::SignalingState::Stable:
|
|
91
|
+
return "stable";
|
|
92
|
+
case rtc::PeerConnection::SignalingState::HaveLocalOffer:
|
|
93
|
+
return "have-local-offer";
|
|
94
|
+
case rtc::PeerConnection::SignalingState::HaveRemoteOffer:
|
|
95
|
+
return "have-remote-offer";
|
|
96
|
+
case rtc::PeerConnection::SignalingState::HaveLocalPranswer:
|
|
97
|
+
return "have-local-pranswer";
|
|
98
|
+
case rtc::PeerConnection::SignalingState::HaveRemotePranswer:
|
|
99
|
+
return "have-remote-pranswer";
|
|
100
|
+
}
|
|
101
|
+
return "stable";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
rtc::Description::Type ParseDescriptionType(const std::string &type) {
|
|
105
|
+
if (type.empty())
|
|
106
|
+
return rtc::Description::Type::Unspec;
|
|
107
|
+
return rtc::Description::stringToType(type);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
Napi::Object DescriptionToObject(Napi::Env env, const rtc::Description &description) {
|
|
111
|
+
Napi::Object result = Napi::Object::New(env);
|
|
112
|
+
result.Set("type", description.typeString());
|
|
113
|
+
result.Set("sdp", std::string(description));
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
rtc::LocalDescriptionInit ParseLocalDescriptionInit(const Napi::Value &value) {
|
|
118
|
+
rtc::LocalDescriptionInit init;
|
|
119
|
+
if (!value.IsObject())
|
|
120
|
+
return init;
|
|
121
|
+
|
|
122
|
+
Napi::Object object = value.As<Napi::Object>();
|
|
123
|
+
if (object.Has("iceUfrag") && !object.Get("iceUfrag").IsNull() &&
|
|
124
|
+
!object.Get("iceUfrag").IsUndefined())
|
|
125
|
+
init.iceUfrag = object.Get("iceUfrag").ToString().Utf8Value();
|
|
126
|
+
if (object.Has("icePwd") && !object.Get("icePwd").IsNull() &&
|
|
127
|
+
!object.Get("icePwd").IsUndefined())
|
|
128
|
+
init.icePwd = object.Get("icePwd").ToString().Utf8Value();
|
|
129
|
+
return init;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
std::string OpenSslErrorString() {
|
|
133
|
+
unsigned long error = ERR_get_error();
|
|
134
|
+
if (!error)
|
|
135
|
+
return "unknown OpenSSL error";
|
|
136
|
+
char buffer[256];
|
|
137
|
+
ERR_error_string_n(error, buffer, sizeof(buffer));
|
|
138
|
+
return buffer;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
void CheckOpenSsl(int result, const char *message) {
|
|
142
|
+
if (result != 1)
|
|
143
|
+
throw std::runtime_error(std::string(message) + ": " + OpenSslErrorString());
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
void CheckOpenSslPositive(int result, const char *message) {
|
|
147
|
+
if (result <= 0)
|
|
148
|
+
throw std::runtime_error(std::string(message) + ": " + OpenSslErrorString());
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
struct CertificateMaterial {
|
|
152
|
+
std::string certificatePem;
|
|
153
|
+
std::string keyPem;
|
|
154
|
+
std::string fingerprint;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
using BioPtr = std::unique_ptr<BIO, decltype(&BIO_free)>;
|
|
158
|
+
using EvpPkeyPtr = std::unique_ptr<EVP_PKEY, decltype(&EVP_PKEY_free)>;
|
|
159
|
+
using EvpPkeyCtxPtr = std::unique_ptr<EVP_PKEY_CTX, decltype(&EVP_PKEY_CTX_free)>;
|
|
160
|
+
using X509Ptr = std::unique_ptr<X509, decltype(&X509_free)>;
|
|
161
|
+
|
|
162
|
+
std::string BioToString(BIO *bio) {
|
|
163
|
+
BUF_MEM *memory = nullptr;
|
|
164
|
+
BIO_get_mem_ptr(bio, &memory);
|
|
165
|
+
if (!memory || !memory->data)
|
|
166
|
+
return {};
|
|
167
|
+
return std::string(memory->data, memory->length);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
EvpPkeyPtr GenerateEcKey() {
|
|
171
|
+
EvpPkeyCtxPtr parameterContext(EVP_PKEY_CTX_new_id(EVP_PKEY_EC, nullptr),
|
|
172
|
+
EVP_PKEY_CTX_free);
|
|
173
|
+
if (!parameterContext)
|
|
174
|
+
throw std::runtime_error("Failed to allocate EC parameter context");
|
|
175
|
+
CheckOpenSsl(EVP_PKEY_paramgen_init(parameterContext.get()), "Failed to initialize EC parameters");
|
|
176
|
+
CheckOpenSsl(EVP_PKEY_CTX_set_ec_paramgen_curve_nid(parameterContext.get(),
|
|
177
|
+
NID_X9_62_prime256v1),
|
|
178
|
+
"Failed to set EC curve");
|
|
179
|
+
|
|
180
|
+
EVP_PKEY *parametersRaw = nullptr;
|
|
181
|
+
CheckOpenSsl(EVP_PKEY_paramgen(parameterContext.get(), ¶metersRaw),
|
|
182
|
+
"Failed to generate EC parameters");
|
|
183
|
+
EvpPkeyPtr parameters(parametersRaw, EVP_PKEY_free);
|
|
184
|
+
|
|
185
|
+
EvpPkeyCtxPtr keyContext(EVP_PKEY_CTX_new(parameters.get(), nullptr), EVP_PKEY_CTX_free);
|
|
186
|
+
if (!keyContext)
|
|
187
|
+
throw std::runtime_error("Failed to allocate EC key context");
|
|
188
|
+
CheckOpenSsl(EVP_PKEY_keygen_init(keyContext.get()), "Failed to initialize EC key generation");
|
|
189
|
+
|
|
190
|
+
EVP_PKEY *keyRaw = nullptr;
|
|
191
|
+
CheckOpenSsl(EVP_PKEY_keygen(keyContext.get(), &keyRaw), "Failed to generate EC key");
|
|
192
|
+
return EvpPkeyPtr(keyRaw, EVP_PKEY_free);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
EvpPkeyPtr GenerateRsaKey(uint32_t modulusLength) {
|
|
196
|
+
EvpPkeyCtxPtr keyContext(EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, nullptr), EVP_PKEY_CTX_free);
|
|
197
|
+
if (!keyContext)
|
|
198
|
+
throw std::runtime_error("Failed to allocate RSA key context");
|
|
199
|
+
CheckOpenSsl(EVP_PKEY_keygen_init(keyContext.get()), "Failed to initialize RSA key generation");
|
|
200
|
+
CheckOpenSsl(EVP_PKEY_CTX_set_rsa_keygen_bits(keyContext.get(), static_cast<int>(modulusLength)),
|
|
201
|
+
"Failed to set RSA modulus length");
|
|
202
|
+
|
|
203
|
+
EVP_PKEY *keyRaw = nullptr;
|
|
204
|
+
CheckOpenSsl(EVP_PKEY_keygen(keyContext.get(), &keyRaw), "Failed to generate RSA key");
|
|
205
|
+
return EvpPkeyPtr(keyRaw, EVP_PKEY_free);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
long ExpirationSecondsFromMilliseconds(double expiresMs) {
|
|
209
|
+
if (expiresMs <= 0)
|
|
210
|
+
return 0;
|
|
211
|
+
double seconds = std::ceil(expiresMs / 1000.0);
|
|
212
|
+
constexpr double maxLongSeconds = 2147483647.0;
|
|
213
|
+
if (seconds > maxLongSeconds)
|
|
214
|
+
seconds = maxLongSeconds;
|
|
215
|
+
return static_cast<long>(seconds);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
std::string FingerprintForCertificate(X509 *certificate) {
|
|
219
|
+
unsigned char digest[EVP_MAX_MD_SIZE];
|
|
220
|
+
unsigned int digestLength = 0;
|
|
221
|
+
CheckOpenSsl(X509_digest(certificate, EVP_sha256(), digest, &digestLength),
|
|
222
|
+
"Failed to compute certificate fingerprint");
|
|
223
|
+
|
|
224
|
+
std::ostringstream output;
|
|
225
|
+
output << std::hex << std::nouppercase << std::setfill('0');
|
|
226
|
+
for (unsigned int i = 0; i < digestLength; ++i) {
|
|
227
|
+
if (i)
|
|
228
|
+
output << ':';
|
|
229
|
+
output << std::setw(2) << static_cast<unsigned int>(digest[i]);
|
|
230
|
+
}
|
|
231
|
+
return output.str();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
CertificateMaterial GenerateCertificateMaterial(const std::string &algorithm, uint32_t modulusLength,
|
|
235
|
+
double expiresMs) {
|
|
236
|
+
EvpPkeyPtr key = algorithm == "RSASSA-PKCS1-v1_5" ? GenerateRsaKey(modulusLength)
|
|
237
|
+
: GenerateEcKey();
|
|
238
|
+
X509Ptr certificate(X509_new(), X509_free);
|
|
239
|
+
if (!certificate)
|
|
240
|
+
throw std::runtime_error("Failed to allocate X509 certificate");
|
|
241
|
+
|
|
242
|
+
CheckOpenSsl(X509_set_version(certificate.get(), 2), "Failed to set certificate version");
|
|
243
|
+
auto serial = std::chrono::system_clock::now().time_since_epoch().count();
|
|
244
|
+
ASN1_INTEGER_set(X509_get_serialNumber(certificate.get()), static_cast<long>(serial & 0x7fffffff));
|
|
245
|
+
X509_gmtime_adj(X509_get_notBefore(certificate.get()), -3600);
|
|
246
|
+
X509_gmtime_adj(X509_get_notAfter(certificate.get()),
|
|
247
|
+
ExpirationSecondsFromMilliseconds(expiresMs));
|
|
248
|
+
CheckOpenSsl(X509_set_pubkey(certificate.get(), key.get()), "Failed to set certificate public key");
|
|
249
|
+
|
|
250
|
+
X509_NAME *name = X509_get_subject_name(certificate.get());
|
|
251
|
+
CheckOpenSsl(X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC,
|
|
252
|
+
reinterpret_cast<const unsigned char *>("webrtc-node"),
|
|
253
|
+
-1, -1, 0),
|
|
254
|
+
"Failed to set certificate subject");
|
|
255
|
+
CheckOpenSsl(X509_set_issuer_name(certificate.get(), name), "Failed to set certificate issuer");
|
|
256
|
+
CheckOpenSslPositive(X509_sign(certificate.get(), key.get(), EVP_sha256()),
|
|
257
|
+
"Failed to sign certificate");
|
|
258
|
+
|
|
259
|
+
BioPtr certificateBio(BIO_new(BIO_s_mem()), BIO_free);
|
|
260
|
+
BioPtr keyBio(BIO_new(BIO_s_mem()), BIO_free);
|
|
261
|
+
if (!certificateBio || !keyBio)
|
|
262
|
+
throw std::runtime_error("Failed to allocate PEM BIO");
|
|
263
|
+
CheckOpenSsl(PEM_write_bio_X509(certificateBio.get(), certificate.get()),
|
|
264
|
+
"Failed to encode certificate PEM");
|
|
265
|
+
CheckOpenSsl(PEM_write_bio_PrivateKey(keyBio.get(), key.get(), nullptr, nullptr, 0, nullptr,
|
|
266
|
+
nullptr),
|
|
267
|
+
"Failed to encode private key PEM");
|
|
268
|
+
|
|
269
|
+
return {BioToString(certificateBio.get()), BioToString(keyBio.get()),
|
|
270
|
+
FingerprintForCertificate(certificate.get())};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
struct ChannelOptions {
|
|
274
|
+
bool ordered = true;
|
|
275
|
+
bool negotiated = false;
|
|
276
|
+
std::optional<uint16_t> id;
|
|
277
|
+
std::optional<uint32_t> maxPacketLifeTime;
|
|
278
|
+
std::optional<uint32_t> maxRetransmits;
|
|
279
|
+
std::string protocol;
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
struct NativeEvent {
|
|
283
|
+
std::string target;
|
|
284
|
+
std::string type;
|
|
285
|
+
int channelId = 0;
|
|
286
|
+
std::shared_ptr<ChannelBinding> channel;
|
|
287
|
+
std::string state;
|
|
288
|
+
std::string descriptionType;
|
|
289
|
+
std::string sdp;
|
|
290
|
+
std::string candidate;
|
|
291
|
+
std::string mid;
|
|
292
|
+
std::string error;
|
|
293
|
+
bool binary = false;
|
|
294
|
+
std::string text;
|
|
295
|
+
std::vector<uint8_t> bytes;
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
struct EventDispatcher : public std::enable_shared_from_this<EventDispatcher> {
|
|
299
|
+
static std::shared_ptr<EventDispatcher> Create(Napi::Env env, Napi::Function callback) {
|
|
300
|
+
return std::shared_ptr<EventDispatcher>(new EventDispatcher(env, callback));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
~EventDispatcher() { Close(); }
|
|
304
|
+
|
|
305
|
+
void Emit(NativeEvent event) {
|
|
306
|
+
auto *queued = new NativeEvent(std::move(event));
|
|
307
|
+
std::lock_guard<std::mutex> lock(lifecycleMutex);
|
|
308
|
+
if (!active.load()) {
|
|
309
|
+
delete queued;
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
napi_status status = tsfn.NonBlockingCall(queued, Dispatch);
|
|
314
|
+
if (status != napi_ok)
|
|
315
|
+
delete queued;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
void Close() {
|
|
319
|
+
std::lock_guard<std::mutex> lock(lifecycleMutex);
|
|
320
|
+
if (active.exchange(false))
|
|
321
|
+
tsfn.Release();
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
private:
|
|
325
|
+
EventDispatcher(Napi::Env env, Napi::Function callback)
|
|
326
|
+
: tsfn(Napi::ThreadSafeFunction::New(env, callback, "webrtc-node events", 0, 1)) {}
|
|
327
|
+
|
|
328
|
+
static void Dispatch(Napi::Env env, Napi::Function callback, NativeEvent *event);
|
|
329
|
+
|
|
330
|
+
std::atomic<bool> active{true};
|
|
331
|
+
std::mutex lifecycleMutex;
|
|
332
|
+
Napi::ThreadSafeFunction tsfn;
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
struct ChannelBinding : public std::enable_shared_from_this<ChannelBinding> {
|
|
336
|
+
static std::shared_ptr<ChannelBinding> Create(std::shared_ptr<rtc::DataChannel> dataChannel,
|
|
337
|
+
std::shared_ptr<EventDispatcher> dispatcher,
|
|
338
|
+
ChannelOptions options) {
|
|
339
|
+
auto binding = std::shared_ptr<ChannelBinding>(
|
|
340
|
+
new ChannelBinding(std::move(dataChannel), std::move(dispatcher), std::move(options)));
|
|
341
|
+
binding->AttachCallbacks();
|
|
342
|
+
return binding;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
static ChannelOptions IncomingOptions(const std::shared_ptr<rtc::DataChannel> &dataChannel) {
|
|
346
|
+
ChannelOptions options;
|
|
347
|
+
options.negotiated = false;
|
|
348
|
+
options.protocol = dataChannel->protocol();
|
|
349
|
+
rtc::Reliability reliability = dataChannel->reliability();
|
|
350
|
+
options.ordered = !reliability.unordered;
|
|
351
|
+
if (reliability.maxPacketLifeTime)
|
|
352
|
+
options.maxPacketLifeTime =
|
|
353
|
+
static_cast<uint32_t>(reliability.maxPacketLifeTime->count());
|
|
354
|
+
if (reliability.maxRetransmits)
|
|
355
|
+
options.maxRetransmits = static_cast<uint32_t>(*reliability.maxRetransmits);
|
|
356
|
+
return options;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
void Close() {
|
|
360
|
+
auto dc = dataChannel;
|
|
361
|
+
if (!dc)
|
|
362
|
+
return;
|
|
363
|
+
std::thread([dc = std::move(dc)]() {
|
|
364
|
+
try {
|
|
365
|
+
dc->close();
|
|
366
|
+
} catch (...) {
|
|
367
|
+
}
|
|
368
|
+
}).detach();
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
void Destroy() {
|
|
372
|
+
{
|
|
373
|
+
std::lock_guard<std::mutex> lock(callbacksMutex);
|
|
374
|
+
if (!callbacksActive.exchange(false))
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
auto dc = dataChannel;
|
|
378
|
+
if (dc)
|
|
379
|
+
dc->resetCallbacks();
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const int id;
|
|
383
|
+
std::shared_ptr<rtc::DataChannel> dataChannel;
|
|
384
|
+
std::shared_ptr<EventDispatcher> dispatcher;
|
|
385
|
+
ChannelOptions options;
|
|
386
|
+
|
|
387
|
+
private:
|
|
388
|
+
ChannelBinding(std::shared_ptr<rtc::DataChannel> dataChannel_,
|
|
389
|
+
std::shared_ptr<EventDispatcher> dispatcher_, ChannelOptions options_)
|
|
390
|
+
: id(nextChannelId.fetch_add(1)), dataChannel(std::move(dataChannel_)),
|
|
391
|
+
dispatcher(std::move(dispatcher_)), options(std::move(options_)) {}
|
|
392
|
+
|
|
393
|
+
void Emit(NativeEvent event) {
|
|
394
|
+
std::lock_guard<std::mutex> lock(callbacksMutex);
|
|
395
|
+
if (!callbacksActive.load())
|
|
396
|
+
return;
|
|
397
|
+
dispatcher->Emit(std::move(event));
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
void AttachCallbacks() {
|
|
401
|
+
std::weak_ptr<ChannelBinding> weak = shared_from_this();
|
|
402
|
+
|
|
403
|
+
dataChannel->onOpen([weak]() {
|
|
404
|
+
if (auto self = weak.lock()) {
|
|
405
|
+
NativeEvent event;
|
|
406
|
+
event.target = "datachannel";
|
|
407
|
+
event.type = "open";
|
|
408
|
+
event.channelId = self->id;
|
|
409
|
+
self->Emit(std::move(event));
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
dataChannel->onClosed([weak]() {
|
|
414
|
+
if (auto self = weak.lock()) {
|
|
415
|
+
NativeEvent event;
|
|
416
|
+
event.target = "datachannel";
|
|
417
|
+
event.type = "close";
|
|
418
|
+
event.channelId = self->id;
|
|
419
|
+
self->Emit(std::move(event));
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
dataChannel->onError([weak](std::string error) {
|
|
424
|
+
if (auto self = weak.lock()) {
|
|
425
|
+
NativeEvent event;
|
|
426
|
+
event.target = "datachannel";
|
|
427
|
+
event.type = "error";
|
|
428
|
+
event.channelId = self->id;
|
|
429
|
+
event.error = std::move(error);
|
|
430
|
+
self->Emit(std::move(event));
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
dataChannel->onBufferedAmountLow([weak]() {
|
|
435
|
+
if (auto self = weak.lock()) {
|
|
436
|
+
NativeEvent event;
|
|
437
|
+
event.target = "datachannel";
|
|
438
|
+
event.type = "bufferedamountlow";
|
|
439
|
+
event.channelId = self->id;
|
|
440
|
+
self->Emit(std::move(event));
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
dataChannel->onMessage([weak](rtc::message_variant data) {
|
|
445
|
+
if (auto self = weak.lock()) {
|
|
446
|
+
NativeEvent event;
|
|
447
|
+
event.target = "datachannel";
|
|
448
|
+
event.type = "message";
|
|
449
|
+
event.channelId = self->id;
|
|
450
|
+
if (std::holds_alternative<std::string>(data)) {
|
|
451
|
+
event.binary = false;
|
|
452
|
+
event.text = std::get<std::string>(std::move(data));
|
|
453
|
+
} else {
|
|
454
|
+
event.binary = true;
|
|
455
|
+
const auto &binary = std::get<rtc::binary>(data);
|
|
456
|
+
event.bytes.reserve(binary.size());
|
|
457
|
+
for (std::byte value : binary)
|
|
458
|
+
event.bytes.push_back(std::to_integer<uint8_t>(value));
|
|
459
|
+
}
|
|
460
|
+
self->Emit(std::move(event));
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
std::atomic<bool> callbacksActive{true};
|
|
466
|
+
std::mutex callbacksMutex;
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
class NativeDataChannel : public Napi::ObjectWrap<NativeDataChannel> {
|
|
470
|
+
public:
|
|
471
|
+
static Napi::FunctionReference constructor;
|
|
472
|
+
|
|
473
|
+
static void Init(Napi::Env env, Napi::Object exports) {
|
|
474
|
+
Napi::Function func = DefineClass(
|
|
475
|
+
env, "NativeDataChannel",
|
|
476
|
+
{
|
|
477
|
+
InstanceMethod("sendString", &NativeDataChannel::SendString),
|
|
478
|
+
InstanceMethod("sendBinary", &NativeDataChannel::SendBinary),
|
|
479
|
+
InstanceMethod("close", &NativeDataChannel::Close),
|
|
480
|
+
InstanceMethod("setBufferedAmountLowThreshold",
|
|
481
|
+
&NativeDataChannel::SetBufferedAmountLowThreshold),
|
|
482
|
+
InstanceAccessor("bindingId", &NativeDataChannel::GetBindingId, nullptr),
|
|
483
|
+
InstanceAccessor("id", &NativeDataChannel::GetId, nullptr),
|
|
484
|
+
InstanceAccessor("label", &NativeDataChannel::GetLabel, nullptr),
|
|
485
|
+
InstanceAccessor("protocol", &NativeDataChannel::GetProtocol, nullptr),
|
|
486
|
+
InstanceAccessor("ordered", &NativeDataChannel::GetOrdered, nullptr),
|
|
487
|
+
InstanceAccessor("negotiated", &NativeDataChannel::GetNegotiated, nullptr),
|
|
488
|
+
InstanceAccessor("maxPacketLifeTime", &NativeDataChannel::GetMaxPacketLifeTime,
|
|
489
|
+
nullptr),
|
|
490
|
+
InstanceAccessor("maxRetransmits", &NativeDataChannel::GetMaxRetransmits, nullptr),
|
|
491
|
+
InstanceAccessor("bufferedAmount", &NativeDataChannel::GetBufferedAmount, nullptr),
|
|
492
|
+
InstanceAccessor("isOpen", &NativeDataChannel::GetIsOpen, nullptr),
|
|
493
|
+
InstanceAccessor("isClosed", &NativeDataChannel::GetIsClosed, nullptr),
|
|
494
|
+
InstanceAccessor("maxMessageSize", &NativeDataChannel::GetMaxMessageSize, nullptr),
|
|
495
|
+
});
|
|
496
|
+
constructor = Napi::Persistent(func);
|
|
497
|
+
constructor.SuppressDestruct();
|
|
498
|
+
exports.Set("NativeDataChannel", func);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
static Napi::Object NewInstance(Napi::Env env, std::shared_ptr<ChannelBinding> binding) {
|
|
502
|
+
auto *payload = new std::shared_ptr<ChannelBinding>(std::move(binding));
|
|
503
|
+
auto external = Napi::External<std::shared_ptr<ChannelBinding>>::New(
|
|
504
|
+
env, payload, [](Napi::Env, std::shared_ptr<ChannelBinding> *data) { delete data; });
|
|
505
|
+
return constructor.New({external});
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
NativeDataChannel(const Napi::CallbackInfo &info) : Napi::ObjectWrap<NativeDataChannel>(info) {
|
|
509
|
+
if (!info[0].IsExternal())
|
|
510
|
+
throw Napi::TypeError::New(info.Env(), "NativeDataChannel requires a native binding");
|
|
511
|
+
binding_ = *info[0].As<Napi::External<std::shared_ptr<ChannelBinding>>>().Data();
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
private:
|
|
515
|
+
std::shared_ptr<ChannelBinding> binding_;
|
|
516
|
+
|
|
517
|
+
Napi::Value SendString(const Napi::CallbackInfo &info) {
|
|
518
|
+
Napi::Env env = info.Env();
|
|
519
|
+
try {
|
|
520
|
+
std::string value = info[0].ToString().Utf8Value();
|
|
521
|
+
bool sent = binding_->dataChannel->send(value);
|
|
522
|
+
return Napi::Boolean::New(env, sent);
|
|
523
|
+
} catch (const std::exception &e) {
|
|
524
|
+
Napi::Error::New(env, e.what()).ThrowAsJavaScriptException();
|
|
525
|
+
return env.Undefined();
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
Napi::Value SendBinary(const Napi::CallbackInfo &info) {
|
|
530
|
+
Napi::Env env = info.Env();
|
|
531
|
+
try {
|
|
532
|
+
if (!info[0].IsTypedArray())
|
|
533
|
+
throw std::invalid_argument("sendBinary expects a Uint8Array");
|
|
534
|
+
auto view = info[0].As<Napi::Uint8Array>();
|
|
535
|
+
const auto *bytes = reinterpret_cast<const rtc::byte *>(view.Data());
|
|
536
|
+
bool sent = binding_->dataChannel->send(bytes, view.ByteLength());
|
|
537
|
+
return Napi::Boolean::New(env, sent);
|
|
538
|
+
} catch (const std::exception &e) {
|
|
539
|
+
Napi::Error::New(env, e.what()).ThrowAsJavaScriptException();
|
|
540
|
+
return env.Undefined();
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
Napi::Value Close(const Napi::CallbackInfo &info) {
|
|
545
|
+
try {
|
|
546
|
+
binding_->Close();
|
|
547
|
+
} catch (const std::exception &e) {
|
|
548
|
+
Napi::Error::New(info.Env(), e.what()).ThrowAsJavaScriptException();
|
|
549
|
+
}
|
|
550
|
+
return info.Env().Undefined();
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
Napi::Value SetBufferedAmountLowThreshold(const Napi::CallbackInfo &info) {
|
|
554
|
+
Napi::Env env = info.Env();
|
|
555
|
+
try {
|
|
556
|
+
size_t value = info[0].ToNumber().Int64Value();
|
|
557
|
+
binding_->dataChannel->setBufferedAmountLowThreshold(value);
|
|
558
|
+
} catch (const std::exception &e) {
|
|
559
|
+
Napi::Error::New(env, e.what()).ThrowAsJavaScriptException();
|
|
560
|
+
}
|
|
561
|
+
return env.Undefined();
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
Napi::Value GetBindingId(const Napi::CallbackInfo &info) {
|
|
565
|
+
return Napi::Number::New(info.Env(), binding_->id);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
Napi::Value GetId(const Napi::CallbackInfo &info) {
|
|
569
|
+
auto id = binding_->dataChannel->id();
|
|
570
|
+
if (!id)
|
|
571
|
+
return info.Env().Null();
|
|
572
|
+
return Napi::Number::New(info.Env(), *id);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
Napi::Value GetLabel(const Napi::CallbackInfo &info) {
|
|
576
|
+
return Napi::String::New(info.Env(), binding_->dataChannel->label());
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
Napi::Value GetProtocol(const Napi::CallbackInfo &info) {
|
|
580
|
+
return Napi::String::New(info.Env(), binding_->dataChannel->protocol());
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
Napi::Value GetOrdered(const Napi::CallbackInfo &info) {
|
|
584
|
+
return Napi::Boolean::New(info.Env(), binding_->options.ordered);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
Napi::Value GetNegotiated(const Napi::CallbackInfo &info) {
|
|
588
|
+
return Napi::Boolean::New(info.Env(), binding_->options.negotiated);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
Napi::Value GetMaxPacketLifeTime(const Napi::CallbackInfo &info) {
|
|
592
|
+
if (!binding_->options.maxPacketLifeTime)
|
|
593
|
+
return info.Env().Null();
|
|
594
|
+
return Napi::Number::New(info.Env(), *binding_->options.maxPacketLifeTime);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
Napi::Value GetMaxRetransmits(const Napi::CallbackInfo &info) {
|
|
598
|
+
if (!binding_->options.maxRetransmits)
|
|
599
|
+
return info.Env().Null();
|
|
600
|
+
return Napi::Number::New(info.Env(), *binding_->options.maxRetransmits);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
Napi::Value GetBufferedAmount(const Napi::CallbackInfo &info) {
|
|
604
|
+
return Napi::Number::New(info.Env(), binding_->dataChannel->bufferedAmount());
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
Napi::Value GetIsOpen(const Napi::CallbackInfo &info) {
|
|
608
|
+
return Napi::Boolean::New(info.Env(), binding_->dataChannel->isOpen());
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
Napi::Value GetIsClosed(const Napi::CallbackInfo &info) {
|
|
612
|
+
return Napi::Boolean::New(info.Env(), binding_->dataChannel->isClosed());
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
Napi::Value GetMaxMessageSize(const Napi::CallbackInfo &info) {
|
|
616
|
+
return Napi::Number::New(info.Env(), binding_->dataChannel->maxMessageSize());
|
|
617
|
+
}
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
Napi::FunctionReference NativeDataChannel::constructor;
|
|
621
|
+
|
|
622
|
+
void EventDispatcher::Dispatch(Napi::Env env, Napi::Function callback, NativeEvent *event) {
|
|
623
|
+
std::unique_ptr<NativeEvent> scoped(event);
|
|
624
|
+
|
|
625
|
+
Napi::Object object = Napi::Object::New(env);
|
|
626
|
+
object.Set("target", scoped->target);
|
|
627
|
+
object.Set("type", scoped->type);
|
|
628
|
+
if (scoped->channelId)
|
|
629
|
+
object.Set("channelId", scoped->channelId);
|
|
630
|
+
if (!scoped->state.empty())
|
|
631
|
+
object.Set("state", scoped->state);
|
|
632
|
+
if (!scoped->descriptionType.empty()) {
|
|
633
|
+
Napi::Object description = Napi::Object::New(env);
|
|
634
|
+
description.Set("type", scoped->descriptionType);
|
|
635
|
+
description.Set("sdp", scoped->sdp);
|
|
636
|
+
object.Set("description", description);
|
|
637
|
+
}
|
|
638
|
+
if (!scoped->candidate.empty() || !scoped->mid.empty()) {
|
|
639
|
+
Napi::Object candidate = Napi::Object::New(env);
|
|
640
|
+
candidate.Set("candidate", scoped->candidate);
|
|
641
|
+
if (!scoped->mid.empty())
|
|
642
|
+
candidate.Set("sdpMid", scoped->mid);
|
|
643
|
+
object.Set("candidate", candidate);
|
|
644
|
+
}
|
|
645
|
+
if (!scoped->error.empty())
|
|
646
|
+
object.Set("error", scoped->error);
|
|
647
|
+
if (scoped->type == "message") {
|
|
648
|
+
object.Set("binary", scoped->binary);
|
|
649
|
+
if (scoped->binary) {
|
|
650
|
+
Napi::ArrayBuffer buffer = Napi::ArrayBuffer::New(env, scoped->bytes.size());
|
|
651
|
+
if (!scoped->bytes.empty())
|
|
652
|
+
std::memcpy(buffer.Data(), scoped->bytes.data(), scoped->bytes.size());
|
|
653
|
+
object.Set("data", buffer);
|
|
654
|
+
} else {
|
|
655
|
+
object.Set("data", scoped->text);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
if (scoped->channel) {
|
|
659
|
+
object.Set("channel", NativeDataChannel::NewInstance(env, scoped->channel));
|
|
660
|
+
object.Set("channelId", scoped->channel->id);
|
|
661
|
+
object.Set("channelReadyState", "open");
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
callback.Call({object});
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
Napi::Object CandidateToObject(Napi::Env env, const rtc::Candidate &candidate) {
|
|
668
|
+
Napi::Object object = Napi::Object::New(env);
|
|
669
|
+
object.Set("candidate", candidate.candidate());
|
|
670
|
+
object.Set("sdpMid", candidate.mid());
|
|
671
|
+
return object;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
rtc::Configuration ParseConfiguration(const Napi::CallbackInfo &info) {
|
|
675
|
+
rtc::Configuration config;
|
|
676
|
+
config.disableAutoNegotiation = true;
|
|
677
|
+
config.disableAutoGathering = false;
|
|
678
|
+
|
|
679
|
+
if (info.Length() == 0 || !info[0].IsObject())
|
|
680
|
+
return config;
|
|
681
|
+
|
|
682
|
+
Napi::Object input = info[0].As<Napi::Object>();
|
|
683
|
+
if (input.Has("iceTransportPolicy")) {
|
|
684
|
+
std::string policy = input.Get("iceTransportPolicy").ToString().Utf8Value();
|
|
685
|
+
if (policy == "relay")
|
|
686
|
+
config.iceTransportPolicy = rtc::TransportPolicy::Relay;
|
|
687
|
+
}
|
|
688
|
+
if (input.Has("iceServers") && input.Get("iceServers").IsArray()) {
|
|
689
|
+
Napi::Array servers = input.Get("iceServers").As<Napi::Array>();
|
|
690
|
+
for (uint32_t i = 0; i < servers.Length(); ++i) {
|
|
691
|
+
Napi::Value value = servers.Get(i);
|
|
692
|
+
if (!value.IsObject())
|
|
693
|
+
continue;
|
|
694
|
+
Napi::Object server = value.As<Napi::Object>();
|
|
695
|
+
if (!server.Has("urls"))
|
|
696
|
+
continue;
|
|
697
|
+
Napi::Value urls = server.Get("urls");
|
|
698
|
+
if (urls.IsArray()) {
|
|
699
|
+
Napi::Array urlArray = urls.As<Napi::Array>();
|
|
700
|
+
for (uint32_t j = 0; j < urlArray.Length(); ++j) {
|
|
701
|
+
if (urlArray.Get(j).IsString())
|
|
702
|
+
config.iceServers.emplace_back(urlArray.Get(j).ToString().Utf8Value());
|
|
703
|
+
}
|
|
704
|
+
} else if (urls.IsString()) {
|
|
705
|
+
config.iceServers.emplace_back(urls.ToString().Utf8Value());
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
if (input.Has("certificates") && input.Get("certificates").IsArray()) {
|
|
710
|
+
Napi::Array certificates = input.Get("certificates").As<Napi::Array>();
|
|
711
|
+
if (certificates.Length() > 0 && certificates.Get(uint32_t{0}).IsObject()) {
|
|
712
|
+
Napi::Object certificate = certificates.Get(uint32_t{0}).As<Napi::Object>();
|
|
713
|
+
if (certificate.Has("_certificatePem") && certificate.Has("_keyPem") &&
|
|
714
|
+
certificate.Get("_certificatePem").IsString() &&
|
|
715
|
+
certificate.Get("_keyPem").IsString()) {
|
|
716
|
+
config.certificatePemFile = certificate.Get("_certificatePem").ToString().Utf8Value();
|
|
717
|
+
config.keyPemFile = certificate.Get("_keyPem").ToString().Utf8Value();
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
return config;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
ChannelOptions ParseChannelOptions(const Napi::Value &value) {
|
|
726
|
+
ChannelOptions options;
|
|
727
|
+
if (!value.IsObject())
|
|
728
|
+
return options;
|
|
729
|
+
|
|
730
|
+
Napi::Object input = value.As<Napi::Object>();
|
|
731
|
+
if (input.Has("ordered") && input.Get("ordered").IsBoolean())
|
|
732
|
+
options.ordered = input.Get("ordered").ToBoolean().Value();
|
|
733
|
+
if (input.Has("negotiated") && input.Get("negotiated").IsBoolean())
|
|
734
|
+
options.negotiated = input.Get("negotiated").ToBoolean().Value();
|
|
735
|
+
if (input.Has("protocol") && !input.Get("protocol").IsUndefined() &&
|
|
736
|
+
!input.Get("protocol").IsNull())
|
|
737
|
+
options.protocol = input.Get("protocol").ToString().Utf8Value();
|
|
738
|
+
if (input.Has("id") && !input.Get("id").IsUndefined() && !input.Get("id").IsNull())
|
|
739
|
+
options.id = static_cast<uint16_t>(input.Get("id").ToNumber().Uint32Value());
|
|
740
|
+
if (input.Has("maxPacketLifeTime") && !input.Get("maxPacketLifeTime").IsUndefined() &&
|
|
741
|
+
!input.Get("maxPacketLifeTime").IsNull())
|
|
742
|
+
options.maxPacketLifeTime = input.Get("maxPacketLifeTime").ToNumber().Uint32Value();
|
|
743
|
+
if (input.Has("maxRetransmits") && !input.Get("maxRetransmits").IsUndefined() &&
|
|
744
|
+
!input.Get("maxRetransmits").IsNull())
|
|
745
|
+
options.maxRetransmits = input.Get("maxRetransmits").ToNumber().Uint32Value();
|
|
746
|
+
if (options.maxPacketLifeTime && options.maxRetransmits)
|
|
747
|
+
throw std::invalid_argument("maxPacketLifeTime and maxRetransmits are mutually exclusive");
|
|
748
|
+
return options;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
rtc::DataChannelInit ToRtcInit(const ChannelOptions &options) {
|
|
752
|
+
rtc::DataChannelInit init;
|
|
753
|
+
init.negotiated = options.negotiated;
|
|
754
|
+
init.protocol = options.protocol;
|
|
755
|
+
if (options.id)
|
|
756
|
+
init.id = *options.id;
|
|
757
|
+
init.reliability.unordered = !options.ordered;
|
|
758
|
+
if (options.maxPacketLifeTime)
|
|
759
|
+
init.reliability.maxPacketLifeTime = std::chrono::milliseconds(*options.maxPacketLifeTime);
|
|
760
|
+
if (options.maxRetransmits)
|
|
761
|
+
init.reliability.maxRetransmits = *options.maxRetransmits;
|
|
762
|
+
return init;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
struct PeerBinding : public std::enable_shared_from_this<PeerBinding> {
|
|
766
|
+
static std::shared_ptr<PeerBinding> Create(rtc::Configuration config,
|
|
767
|
+
std::shared_ptr<EventDispatcher> dispatcher) {
|
|
768
|
+
auto binding =
|
|
769
|
+
std::shared_ptr<PeerBinding>(new PeerBinding(std::move(config), std::move(dispatcher)));
|
|
770
|
+
binding->AttachCallbacks();
|
|
771
|
+
return binding;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
std::shared_ptr<ChannelBinding> AddChannel(std::shared_ptr<rtc::DataChannel> dataChannel,
|
|
775
|
+
ChannelOptions options) {
|
|
776
|
+
auto channel = ChannelBinding::Create(std::move(dataChannel), dispatcher, std::move(options));
|
|
777
|
+
std::lock_guard<std::mutex> lock(channelsMutex);
|
|
778
|
+
channels[channel->id] = channel;
|
|
779
|
+
return channel;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
void ClosePeer() {
|
|
783
|
+
auto self = shared_from_this();
|
|
784
|
+
std::thread([self = std::move(self)]() {
|
|
785
|
+
try {
|
|
786
|
+
// libdatachannel's public close() is graceful SCTP shutdown; releasing the
|
|
787
|
+
// peer runs the destructor path that closes transports and joins processors.
|
|
788
|
+
self->Shutdown(true);
|
|
789
|
+
} catch (...) {
|
|
790
|
+
}
|
|
791
|
+
}).detach();
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
void Destroy() {
|
|
795
|
+
Shutdown(true);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
std::shared_ptr<rtc::PeerConnection> peerConnection;
|
|
799
|
+
std::shared_ptr<EventDispatcher> dispatcher;
|
|
800
|
+
|
|
801
|
+
private:
|
|
802
|
+
PeerBinding(rtc::Configuration config, std::shared_ptr<EventDispatcher> dispatcher_)
|
|
803
|
+
: peerConnection(std::make_shared<rtc::PeerConnection>(std::move(config))),
|
|
804
|
+
dispatcher(std::move(dispatcher_)) {}
|
|
805
|
+
|
|
806
|
+
void Emit(NativeEvent event) {
|
|
807
|
+
std::lock_guard<std::mutex> lock(callbacksMutex);
|
|
808
|
+
if (!callbacksActive.load())
|
|
809
|
+
return;
|
|
810
|
+
dispatcher->Emit(std::move(event));
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
void DeactivateCallbacks() {
|
|
814
|
+
{
|
|
815
|
+
std::lock_guard<std::mutex> lock(callbacksMutex);
|
|
816
|
+
if (!callbacksActive.exchange(false))
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
if (peerConnection)
|
|
820
|
+
peerConnection->resetCallbacks();
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
void Shutdown(bool releaseNative) {
|
|
824
|
+
if (shutdown.exchange(true))
|
|
825
|
+
return;
|
|
826
|
+
|
|
827
|
+
std::vector<std::shared_ptr<ChannelBinding>> channelSnapshot;
|
|
828
|
+
{
|
|
829
|
+
std::lock_guard<std::mutex> lock(channelsMutex);
|
|
830
|
+
channelSnapshot.reserve(channels.size());
|
|
831
|
+
for (auto &[_, channel] : channels)
|
|
832
|
+
channelSnapshot.push_back(channel);
|
|
833
|
+
if (releaseNative)
|
|
834
|
+
channels.clear();
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
for (auto &channel : channelSnapshot)
|
|
838
|
+
channel->Destroy();
|
|
839
|
+
DeactivateCallbacks();
|
|
840
|
+
dispatcher->Close();
|
|
841
|
+
if (releaseNative) {
|
|
842
|
+
peerConnection.reset();
|
|
843
|
+
} else if (peerConnection) {
|
|
844
|
+
peerConnection->close();
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
void AttachCallbacks() {
|
|
849
|
+
std::weak_ptr<PeerBinding> weak = shared_from_this();
|
|
850
|
+
|
|
851
|
+
peerConnection->onLocalDescription([weak](rtc::Description description) {
|
|
852
|
+
if (auto self = weak.lock()) {
|
|
853
|
+
NativeEvent event;
|
|
854
|
+
event.target = "peerconnection";
|
|
855
|
+
event.type = "localdescription";
|
|
856
|
+
event.descriptionType = description.typeString();
|
|
857
|
+
event.sdp = std::string(description);
|
|
858
|
+
self->Emit(std::move(event));
|
|
859
|
+
}
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
peerConnection->onLocalCandidate([weak](rtc::Candidate candidate) {
|
|
863
|
+
if (auto self = weak.lock()) {
|
|
864
|
+
NativeEvent event;
|
|
865
|
+
event.target = "peerconnection";
|
|
866
|
+
event.type = "localcandidate";
|
|
867
|
+
event.candidate = candidate.candidate();
|
|
868
|
+
event.mid = candidate.mid();
|
|
869
|
+
self->Emit(std::move(event));
|
|
870
|
+
}
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
peerConnection->onStateChange([weak](rtc::PeerConnection::State state) {
|
|
874
|
+
if (auto self = weak.lock()) {
|
|
875
|
+
NativeEvent event;
|
|
876
|
+
event.target = "peerconnection";
|
|
877
|
+
event.type = "connectionstatechange";
|
|
878
|
+
event.state = ToString(state);
|
|
879
|
+
self->Emit(std::move(event));
|
|
880
|
+
}
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
peerConnection->onIceStateChange([weak](rtc::PeerConnection::IceState state) {
|
|
884
|
+
if (auto self = weak.lock()) {
|
|
885
|
+
NativeEvent event;
|
|
886
|
+
event.target = "peerconnection";
|
|
887
|
+
event.type = "iceconnectionstatechange";
|
|
888
|
+
event.state = ToString(state);
|
|
889
|
+
self->Emit(std::move(event));
|
|
890
|
+
}
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
peerConnection->onGatheringStateChange([weak](rtc::PeerConnection::GatheringState state) {
|
|
894
|
+
if (auto self = weak.lock()) {
|
|
895
|
+
NativeEvent event;
|
|
896
|
+
event.target = "peerconnection";
|
|
897
|
+
event.type = "icegatheringstatechange";
|
|
898
|
+
event.state = ToString(state);
|
|
899
|
+
self->Emit(std::move(event));
|
|
900
|
+
}
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
peerConnection->onSignalingStateChange([weak](rtc::PeerConnection::SignalingState state) {
|
|
904
|
+
if (auto self = weak.lock()) {
|
|
905
|
+
NativeEvent event;
|
|
906
|
+
event.target = "peerconnection";
|
|
907
|
+
event.type = "signalingstatechange";
|
|
908
|
+
event.state = ToString(state);
|
|
909
|
+
self->Emit(std::move(event));
|
|
910
|
+
}
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
peerConnection->onDataChannel([weak](std::shared_ptr<rtc::DataChannel> dataChannel) {
|
|
914
|
+
if (auto self = weak.lock()) {
|
|
915
|
+
auto channel =
|
|
916
|
+
self->AddChannel(dataChannel, ChannelBinding::IncomingOptions(dataChannel));
|
|
917
|
+
NativeEvent event;
|
|
918
|
+
event.target = "peerconnection";
|
|
919
|
+
event.type = "datachannel";
|
|
920
|
+
event.channelId = channel->id;
|
|
921
|
+
event.channel = std::move(channel);
|
|
922
|
+
self->Emit(std::move(event));
|
|
923
|
+
}
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
std::atomic<bool> shutdown{false};
|
|
928
|
+
std::atomic<bool> callbacksActive{true};
|
|
929
|
+
std::mutex callbacksMutex;
|
|
930
|
+
std::mutex channelsMutex;
|
|
931
|
+
std::unordered_map<int, std::shared_ptr<ChannelBinding>> channels;
|
|
932
|
+
};
|
|
933
|
+
|
|
934
|
+
class NativePeerConnection : public Napi::ObjectWrap<NativePeerConnection> {
|
|
935
|
+
public:
|
|
936
|
+
static Napi::FunctionReference constructor;
|
|
937
|
+
|
|
938
|
+
static void Init(Napi::Env env, Napi::Object exports) {
|
|
939
|
+
Napi::Function func = DefineClass(
|
|
940
|
+
env, "NativePeerConnection",
|
|
941
|
+
{
|
|
942
|
+
InstanceMethod("createDataChannel", &NativePeerConnection::CreateDataChannel),
|
|
943
|
+
InstanceMethod("createOffer", &NativePeerConnection::CreateOffer),
|
|
944
|
+
InstanceMethod("createAnswer", &NativePeerConnection::CreateAnswer),
|
|
945
|
+
InstanceMethod("setLocalDescription", &NativePeerConnection::SetLocalDescription),
|
|
946
|
+
InstanceMethod("setRemoteDescription", &NativePeerConnection::SetRemoteDescription),
|
|
947
|
+
InstanceMethod("addRemoteCandidate", &NativePeerConnection::AddRemoteCandidate),
|
|
948
|
+
InstanceMethod("gatherLocalCandidates", &NativePeerConnection::GatherLocalCandidates),
|
|
949
|
+
InstanceMethod("localDescription", &NativePeerConnection::LocalDescription),
|
|
950
|
+
InstanceMethod("remoteDescription", &NativePeerConnection::RemoteDescription),
|
|
951
|
+
InstanceMethod("selectedCandidatePair", &NativePeerConnection::SelectedCandidatePair),
|
|
952
|
+
InstanceMethod("close", &NativePeerConnection::Close),
|
|
953
|
+
InstanceAccessor("connectionState", &NativePeerConnection::GetConnectionState, nullptr),
|
|
954
|
+
InstanceAccessor("iceConnectionState", &NativePeerConnection::GetIceConnectionState,
|
|
955
|
+
nullptr),
|
|
956
|
+
InstanceAccessor("iceGatheringState", &NativePeerConnection::GetIceGatheringState,
|
|
957
|
+
nullptr),
|
|
958
|
+
InstanceAccessor("signalingState", &NativePeerConnection::GetSignalingState, nullptr),
|
|
959
|
+
InstanceAccessor("remoteMaxMessageSize",
|
|
960
|
+
&NativePeerConnection::GetRemoteMaxMessageSize, nullptr),
|
|
961
|
+
InstanceAccessor("maxDataChannelId", &NativePeerConnection::GetMaxDataChannelId,
|
|
962
|
+
nullptr),
|
|
963
|
+
});
|
|
964
|
+
constructor = Napi::Persistent(func);
|
|
965
|
+
constructor.SuppressDestruct();
|
|
966
|
+
exports.Set("NativePeerConnection", func);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
NativePeerConnection(const Napi::CallbackInfo &info)
|
|
970
|
+
: Napi::ObjectWrap<NativePeerConnection>(info) {
|
|
971
|
+
Napi::Env env = info.Env();
|
|
972
|
+
if (info.Length() < 2 || !info[1].IsFunction())
|
|
973
|
+
throw Napi::TypeError::New(env, "NativePeerConnection requires an event callback");
|
|
974
|
+
auto dispatcher = EventDispatcher::Create(env, info[1].As<Napi::Function>());
|
|
975
|
+
binding_ = PeerBinding::Create(ParseConfiguration(info), dispatcher);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
~NativePeerConnection() override {
|
|
979
|
+
if (binding_)
|
|
980
|
+
binding_->Destroy();
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
private:
|
|
984
|
+
std::shared_ptr<PeerBinding> binding_;
|
|
985
|
+
|
|
986
|
+
Napi::Value CreateDataChannel(const Napi::CallbackInfo &info) {
|
|
987
|
+
Napi::Env env = info.Env();
|
|
988
|
+
try {
|
|
989
|
+
std::string label = info[0].ToString().Utf8Value();
|
|
990
|
+
ChannelOptions options =
|
|
991
|
+
ParseChannelOptions(info.Length() > 1 ? info[1] : env.Undefined());
|
|
992
|
+
auto dataChannel =
|
|
993
|
+
binding_->peerConnection->createDataChannel(label, ToRtcInit(options));
|
|
994
|
+
auto channel = binding_->AddChannel(std::move(dataChannel), std::move(options));
|
|
995
|
+
return NativeDataChannel::NewInstance(env, std::move(channel));
|
|
996
|
+
} catch (const std::exception &e) {
|
|
997
|
+
Napi::Error::New(env, e.what()).ThrowAsJavaScriptException();
|
|
998
|
+
return env.Undefined();
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
Napi::Value CreateOffer(const Napi::CallbackInfo &info) {
|
|
1003
|
+
Napi::Env env = info.Env();
|
|
1004
|
+
try {
|
|
1005
|
+
return DescriptionToObject(env, binding_->peerConnection->createOffer());
|
|
1006
|
+
} catch (const std::exception &e) {
|
|
1007
|
+
Napi::Error::New(env, e.what()).ThrowAsJavaScriptException();
|
|
1008
|
+
return env.Undefined();
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
Napi::Value CreateAnswer(const Napi::CallbackInfo &info) {
|
|
1013
|
+
Napi::Env env = info.Env();
|
|
1014
|
+
try {
|
|
1015
|
+
return DescriptionToObject(env, binding_->peerConnection->createAnswer());
|
|
1016
|
+
} catch (const std::exception &e) {
|
|
1017
|
+
Napi::Error::New(env, e.what()).ThrowAsJavaScriptException();
|
|
1018
|
+
return env.Undefined();
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
Napi::Value SetLocalDescription(const Napi::CallbackInfo &info) {
|
|
1023
|
+
Napi::Env env = info.Env();
|
|
1024
|
+
try {
|
|
1025
|
+
std::string type;
|
|
1026
|
+
if (info.Length() > 0 && !info[0].IsUndefined() && !info[0].IsNull())
|
|
1027
|
+
type = info[0].ToString().Utf8Value();
|
|
1028
|
+
rtc::LocalDescriptionInit init;
|
|
1029
|
+
if (info.Length() > 1)
|
|
1030
|
+
init = ParseLocalDescriptionInit(info[1]);
|
|
1031
|
+
binding_->peerConnection->setLocalDescription(ParseDescriptionType(type), init);
|
|
1032
|
+
return env.Undefined();
|
|
1033
|
+
} catch (const std::exception &e) {
|
|
1034
|
+
Napi::Error::New(env, e.what()).ThrowAsJavaScriptException();
|
|
1035
|
+
return env.Undefined();
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
Napi::Value SetRemoteDescription(const Napi::CallbackInfo &info) {
|
|
1040
|
+
Napi::Env env = info.Env();
|
|
1041
|
+
try {
|
|
1042
|
+
Napi::Object object = info[0].As<Napi::Object>();
|
|
1043
|
+
std::string type = object.Get("type").ToString().Utf8Value();
|
|
1044
|
+
std::string sdp = object.Get("sdp").ToString().Utf8Value();
|
|
1045
|
+
binding_->peerConnection->setRemoteDescription(rtc::Description(sdp, type));
|
|
1046
|
+
return env.Undefined();
|
|
1047
|
+
} catch (const std::exception &e) {
|
|
1048
|
+
Napi::Error::New(env, e.what()).ThrowAsJavaScriptException();
|
|
1049
|
+
return env.Undefined();
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
Napi::Value AddRemoteCandidate(const Napi::CallbackInfo &info) {
|
|
1054
|
+
Napi::Env env = info.Env();
|
|
1055
|
+
try {
|
|
1056
|
+
Napi::Object object = info[0].As<Napi::Object>();
|
|
1057
|
+
std::string candidate = object.Get("candidate").ToString().Utf8Value();
|
|
1058
|
+
std::string mid;
|
|
1059
|
+
if (object.Has("sdpMid") && !object.Get("sdpMid").IsNull() &&
|
|
1060
|
+
!object.Get("sdpMid").IsUndefined())
|
|
1061
|
+
mid = object.Get("sdpMid").ToString().Utf8Value();
|
|
1062
|
+
binding_->peerConnection->addRemoteCandidate(rtc::Candidate(candidate, mid));
|
|
1063
|
+
return env.Undefined();
|
|
1064
|
+
} catch (const std::exception &e) {
|
|
1065
|
+
Napi::Error::New(env, e.what()).ThrowAsJavaScriptException();
|
|
1066
|
+
return env.Undefined();
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
Napi::Value GatherLocalCandidates(const Napi::CallbackInfo &info) {
|
|
1071
|
+
Napi::Env env = info.Env();
|
|
1072
|
+
try {
|
|
1073
|
+
binding_->peerConnection->gatherLocalCandidates();
|
|
1074
|
+
} catch (const std::exception &e) {
|
|
1075
|
+
Napi::Error::New(env, e.what()).ThrowAsJavaScriptException();
|
|
1076
|
+
}
|
|
1077
|
+
return env.Undefined();
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
Napi::Value LocalDescription(const Napi::CallbackInfo &info) {
|
|
1081
|
+
Napi::Env env = info.Env();
|
|
1082
|
+
try {
|
|
1083
|
+
auto description = binding_->peerConnection->localDescription();
|
|
1084
|
+
if (!description)
|
|
1085
|
+
return env.Null();
|
|
1086
|
+
return DescriptionToObject(env, *description);
|
|
1087
|
+
} catch (const std::exception &e) {
|
|
1088
|
+
Napi::Error::New(env, e.what()).ThrowAsJavaScriptException();
|
|
1089
|
+
return env.Undefined();
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
Napi::Value RemoteDescription(const Napi::CallbackInfo &info) {
|
|
1094
|
+
Napi::Env env = info.Env();
|
|
1095
|
+
try {
|
|
1096
|
+
auto description = binding_->peerConnection->remoteDescription();
|
|
1097
|
+
if (!description)
|
|
1098
|
+
return env.Null();
|
|
1099
|
+
return DescriptionToObject(env, *description);
|
|
1100
|
+
} catch (const std::exception &e) {
|
|
1101
|
+
Napi::Error::New(env, e.what()).ThrowAsJavaScriptException();
|
|
1102
|
+
return env.Undefined();
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
Napi::Value SelectedCandidatePair(const Napi::CallbackInfo &info) {
|
|
1107
|
+
Napi::Env env = info.Env();
|
|
1108
|
+
try {
|
|
1109
|
+
rtc::Candidate local;
|
|
1110
|
+
rtc::Candidate remote;
|
|
1111
|
+
if (!binding_->peerConnection->getSelectedCandidatePair(&local, &remote))
|
|
1112
|
+
return env.Null();
|
|
1113
|
+
Napi::Object pair = Napi::Object::New(env);
|
|
1114
|
+
pair.Set("local", CandidateToObject(env, local));
|
|
1115
|
+
pair.Set("remote", CandidateToObject(env, remote));
|
|
1116
|
+
return pair;
|
|
1117
|
+
} catch (const std::exception &e) {
|
|
1118
|
+
Napi::Error::New(env, e.what()).ThrowAsJavaScriptException();
|
|
1119
|
+
return env.Undefined();
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
Napi::Value Close(const Napi::CallbackInfo &info) {
|
|
1124
|
+
try {
|
|
1125
|
+
binding_->ClosePeer();
|
|
1126
|
+
} catch (const std::exception &e) {
|
|
1127
|
+
Napi::Error::New(info.Env(), e.what()).ThrowAsJavaScriptException();
|
|
1128
|
+
}
|
|
1129
|
+
return info.Env().Undefined();
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
Napi::Value GetConnectionState(const Napi::CallbackInfo &info) {
|
|
1133
|
+
return Napi::String::New(info.Env(), ToString(binding_->peerConnection->state()));
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
Napi::Value GetIceConnectionState(const Napi::CallbackInfo &info) {
|
|
1137
|
+
return Napi::String::New(info.Env(), ToString(binding_->peerConnection->iceState()));
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
Napi::Value GetIceGatheringState(const Napi::CallbackInfo &info) {
|
|
1141
|
+
return Napi::String::New(info.Env(), ToString(binding_->peerConnection->gatheringState()));
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
Napi::Value GetSignalingState(const Napi::CallbackInfo &info) {
|
|
1145
|
+
return Napi::String::New(info.Env(), ToString(binding_->peerConnection->signalingState()));
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
Napi::Value GetRemoteMaxMessageSize(const Napi::CallbackInfo &info) {
|
|
1149
|
+
return Napi::Number::New(info.Env(), binding_->peerConnection->remoteMaxMessageSize());
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
Napi::Value GetMaxDataChannelId(const Napi::CallbackInfo &info) {
|
|
1153
|
+
return Napi::Number::New(info.Env(), binding_->peerConnection->maxDataChannelId());
|
|
1154
|
+
}
|
|
1155
|
+
};
|
|
1156
|
+
|
|
1157
|
+
Napi::FunctionReference NativePeerConnection::constructor;
|
|
1158
|
+
|
|
1159
|
+
Napi::Value GenerateCertificate(const Napi::CallbackInfo &info) {
|
|
1160
|
+
Napi::Env env = info.Env();
|
|
1161
|
+
try {
|
|
1162
|
+
if (info.Length() == 0 || !info[0].IsObject())
|
|
1163
|
+
throw std::invalid_argument("generateCertificate requires an options object");
|
|
1164
|
+
Napi::Object options = info[0].As<Napi::Object>();
|
|
1165
|
+
std::string algorithm = options.Get("algorithm").ToString().Utf8Value();
|
|
1166
|
+
uint32_t modulusLength = options.Has("modulusLength")
|
|
1167
|
+
? options.Get("modulusLength").ToNumber().Uint32Value()
|
|
1168
|
+
: 2048;
|
|
1169
|
+
double expiresMs =
|
|
1170
|
+
options.Has("expiresMs") ? options.Get("expiresMs").ToNumber().DoubleValue()
|
|
1171
|
+
: 30.0 * 24.0 * 60.0 * 60.0 * 1000.0;
|
|
1172
|
+
|
|
1173
|
+
auto material = GenerateCertificateMaterial(algorithm, modulusLength, expiresMs);
|
|
1174
|
+
Napi::Object result = Napi::Object::New(env);
|
|
1175
|
+
result.Set("certificatePem", material.certificatePem);
|
|
1176
|
+
result.Set("keyPem", material.keyPem);
|
|
1177
|
+
result.Set("fingerprints", Napi::Array::New(env, 1));
|
|
1178
|
+
Napi::Object fingerprint = Napi::Object::New(env);
|
|
1179
|
+
fingerprint.Set("algorithm", "sha-256");
|
|
1180
|
+
fingerprint.Set("value", material.fingerprint);
|
|
1181
|
+
result.Get("fingerprints").As<Napi::Array>().Set(uint32_t{0}, fingerprint);
|
|
1182
|
+
return result;
|
|
1183
|
+
} catch (const std::exception &e) {
|
|
1184
|
+
Napi::Error::New(env, e.what()).ThrowAsJavaScriptException();
|
|
1185
|
+
return env.Undefined();
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
Napi::Object InitAll(Napi::Env env, Napi::Object exports) {
|
|
1190
|
+
NativeDataChannel::Init(env, exports);
|
|
1191
|
+
NativePeerConnection::Init(env, exports);
|
|
1192
|
+
exports.Set("generateCertificate", Napi::Function::New(env, GenerateCertificate));
|
|
1193
|
+
exports.Set("cleanup", Napi::Function::New(env, [](const Napi::CallbackInfo &info) {
|
|
1194
|
+
rtc::Cleanup().wait();
|
|
1195
|
+
return info.Env().Undefined();
|
|
1196
|
+
}));
|
|
1197
|
+
return exports;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
} // namespace
|
|
1201
|
+
|
|
1202
|
+
NODE_API_MODULE(webrtc_node, InitAll)
|