janus-whep-server 1.0.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.
Files changed (4) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +122 -0
  3. package/package.json +50 -0
  4. package/src/whep.js +680 -0
package/src/whep.js ADDED
@@ -0,0 +1,680 @@
1
+ 'use strict';
2
+
3
+ /*
4
+ * Simple WHEP server
5
+ *
6
+ * Author: Lorenzo Miniero <lorenzo@meetecho.com>
7
+ * License: GPLv3
8
+ *
9
+ * WHEP API and endpoint management
10
+ *
11
+ */
12
+
13
+ // Dependencies
14
+ import express from 'express';
15
+ import cors from 'cors';
16
+ import fs from 'fs';
17
+ import http from 'http';
18
+ import https from 'https';
19
+ import Janode from 'janode';
20
+ import StreamingPlugin from 'janode/plugins/streaming';
21
+ import { EventEmitter } from 'events';
22
+
23
+ // WHEP server class
24
+ class JanusWhepServer extends EventEmitter {
25
+
26
+ // Constructor
27
+ constructor({ janus, rest, allowTrickle = true, strictETags = false, iceServers = [], debug }) {
28
+ super();
29
+ // Parse configuration
30
+ if(!janus || typeof janus !== 'object')
31
+ throw new Error('Invalid configuration, missing parameter "janus" or not an object');
32
+ if(!janus.address)
33
+ throw new Error('Invalid configuration, missing parameter "address" in "janus"');
34
+ if(!rest || typeof rest !== 'object')
35
+ throw new Error('Invalid configuration, missing parameter "rest" or not an object');
36
+ if(!rest.basePath)
37
+ throw new Error('Invalid configuration, missing parameter "basePath" in "rest"');
38
+ if(!rest.port && !rest.app)
39
+ throw new Error('Invalid configuration, at least one of "port" and "app" should be set in "rest"');
40
+ const debugLevels = [ 'err', 'warn', 'info', 'verb', 'debug' ];
41
+ if(debug && debugLevels.indexOf(debug) === -1)
42
+ throw new Error('Invalid configuration, unsupported "debug" level');
43
+ this.config = {
44
+ janus: {
45
+ address: janus.address
46
+ },
47
+ rest: {
48
+ port: rest.port,
49
+ basePath: rest.basePath,
50
+ app: rest.app
51
+ },
52
+ allowTrickle: (allowTrickle === true),
53
+ strictETags: (strictETags === true),
54
+ iceServers: Array.isArray(iceServers) ? iceServers : [iceServers]
55
+ };
56
+
57
+ // Resources
58
+ this.janus = null;
59
+ this.endpoints = new Map();
60
+ this.subscribers = new Map();
61
+ this.logger = new JanusWhepLogger({ prefix: '[WHEP] ', level: debug ? debugLevels.indexOf(debug) : 2 });
62
+ }
63
+
64
+ async start() {
65
+ if(this.started)
66
+ throw new Error('WHEP server already started');
67
+ // Connect to Janus
68
+ await this._connectToJanus();
69
+ // WHEP REST API
70
+ if(!this.config.rest.app) {
71
+ // Spawn a new app and server
72
+ this.logger.verb('Spawning new Express app');
73
+ let app = express();
74
+ this._setupRest(app);
75
+ let options = null;
76
+ let useHttps = (this.config.rest.https && this.config.rest.https.cert && this.config.rest.https.key);
77
+ if(useHttps) {
78
+ options = {
79
+ cert: fs.readFileSync(this.config.rest.https.cert, 'utf8'),
80
+ key: fs.readFileSync(this.config.rest.https.key, 'utf8'),
81
+ passphrase: this.config.rest.https.passphrase
82
+ };
83
+ }
84
+ this.server = await (useHttps ? https : http).createServer(options, app);
85
+ await this.server.listen(this.config.rest.port);
86
+ } else {
87
+ // A server already exists, only add our endpoints to its router
88
+ this.logger.verb('Reusing existing Express app');
89
+ this._setupRest(this.config.rest.app);
90
+ }
91
+ // We're up and running
92
+ this.logger.info('WHEP server started');
93
+ this.started = true;
94
+ return this;
95
+ }
96
+
97
+ async destroy() {
98
+ if(!this.started)
99
+ throw new Error('WHEP server not started');
100
+ if(this.janus)
101
+ await this.janus.close();
102
+ if(this.server)
103
+ this.server.close();
104
+ }
105
+
106
+ generateRandomString(len) {
107
+ const charSet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
108
+ let randomString = '';
109
+ for(let i=0; i<len; i++) {
110
+ let randomPoz = Math.floor(Math.random() * charSet.length);
111
+ randomString += charSet.substring(randomPoz,randomPoz+1);
112
+ }
113
+ return randomString;
114
+ }
115
+
116
+ createEndpoint({ id, mountpoint, pin, label, token, iceServers }) {
117
+ if(!id || !mountpoint)
118
+ throw new Error('Invalid arguments');
119
+ if(this.endpoints.has(id))
120
+ throw new Error('Endpoint already exists');
121
+ let endpoint = new JanusWhepEndpoint({
122
+ id: id,
123
+ mountpoint: mountpoint,
124
+ pin: pin,
125
+ label: label,
126
+ token: token,
127
+ iceServers: iceServers
128
+ });
129
+ this.logger.info('[' + id + '] Created new WHEP endpoint');
130
+ this.endpoints.set(id, endpoint);
131
+ return endpoint;
132
+ }
133
+
134
+ listEndpoints() {
135
+ let list = [];
136
+ this.endpoints.forEach(function(endpoint, id) {
137
+ list.push({ id: id, subscribers: endpoint.countSubscribers() });
138
+ });
139
+ return list;
140
+ }
141
+
142
+ getEndpoint({ id }) {
143
+ return this.endpoints.get(id);
144
+ }
145
+
146
+ async destroyEndpoint({ id }) {
147
+ let endpoint = this.endpoints.get(id);
148
+ if(!id || !endpoint)
149
+ throw new Error('Invalid endpoint ID');
150
+ // Get rid of the Janus subscribers, if there's any
151
+ endpoint.subscribers.forEach(async function(_val, uuid) {
152
+ let subscriber = this.subscribers.get(uuid);
153
+ if(!subscriber)
154
+ return;
155
+ if(this.janus && subscriber.handle)
156
+ await subscriber.handle.detach().catch(_err => {});
157
+ this.subscribers.delete(uuid);
158
+ }, this);
159
+ this.endpoints.delete(id);
160
+ this.logger.info('[' + id + '] Destroyed WHEP endpoint');
161
+ }
162
+
163
+ countSubscribers() {
164
+ console.log(this.subscribers);
165
+ return this.subscribers.size;
166
+ }
167
+
168
+ listSubscribers() {
169
+ let list = [];
170
+ this.subscribers.forEach(function(subscriber, uuid) {
171
+ list.push({ endpoint: subscriber.whepId, uuid: uuid });
172
+ });
173
+ return list;
174
+
175
+ }
176
+
177
+ // Janus setup
178
+ async _connectToJanus() {
179
+ const connection = await Janode.connect({
180
+ is_admin: false,
181
+ address: {
182
+ url: this.config.janus.address,
183
+ },
184
+ retry_time_secs: 3,
185
+ max_retries: Number.MAX_VALUE
186
+ });
187
+ connection.once(Janode.EVENT.CONNECTION_ERROR, () => {
188
+ this.logger.warn('Lost connectivity to Janus, reset the manager and try reconnecting');
189
+ // Teardown existing endpoints
190
+ this.endpoints.forEach(function(endpoint, id) {
191
+ endpoint.subscribers.forEach(async function(_val, uuid) {
192
+ let subscriber = this.subscribers.get(uuid);
193
+ if(!subscriber)
194
+ return;
195
+ if(subscriber.handle) {
196
+ endpoint.emit('subscriber-gone');
197
+ this.emit('subscriber-gone', id);
198
+ }
199
+ }, this);
200
+ endpoint.subscribers.clear();
201
+ this.logger.info('[' + id + '] Terminating WHEP subscriber sessions');
202
+ endpoint.emit('janus-disconnected');
203
+ }, this);
204
+ this.subscribers.clear();
205
+ this.emit('janus-disconnected');
206
+ // Reconnect
207
+ this.janus = null;
208
+ setTimeout(this._connectToJanus.bind(this), 1);
209
+ });
210
+ this.janus = await connection.create();
211
+ this.logger.info('Connected to Janus:', this.config.janus.address);
212
+ if(this.started)
213
+ this.emit('janus-reconnected');
214
+ }
215
+
216
+ // REST server setup
217
+ _setupRest(app) {
218
+ const router = express.Router();
219
+
220
+ // Just a helper to make sure this API is up and running
221
+ router.get('/healthcheck', (_req, res) => {
222
+ this.logger.debug('/healthcheck');
223
+ res.sendStatus(200);
224
+ });
225
+
226
+ // Subscribe to a WHEP endpoint
227
+ router.post('/endpoint/:id', async (req, res) => {
228
+ let id = req.params.id;
229
+ let endpoint = this.endpoints.get(id);
230
+ if(!id || !endpoint) {
231
+ res.status(404);
232
+ res.send('Invalid endpoint ID');
233
+ return;
234
+ }
235
+ if(endpoint.enabled) {
236
+ res.status(403);
237
+ res.send('Endpoint ID already in use');
238
+ return;
239
+ }
240
+ this.logger.verb('/endpoint/:', id);
241
+ // If we received a payload, make sure it's an SDP
242
+ this.logger.debug(req.body);
243
+ let offer = null;
244
+ if(req.headers['content-type']) {
245
+ if(req.headers['content-type'] !== 'application/sdp' || req.body.indexOf('v=0') < 0) {
246
+ res.status(406);
247
+ res.send('Unsupported content type');
248
+ return;
249
+ }
250
+ offer = req.body;
251
+ }
252
+ // Check the Bearer token
253
+ let auth = req.headers['authorization'];
254
+ if(endpoint.token) {
255
+ if(!auth || auth.indexOf('Bearer ') < 0) {
256
+ res.status(403);
257
+ res.send('Unauthorized');
258
+ return;
259
+ }
260
+ let authtoken = auth.split('Bearer ')[1];
261
+ if(typeof endpoint.token === 'function') {
262
+ if(!endpoint.token(authtoken)) {
263
+ res.status(403);
264
+ res.send('Unauthorized');
265
+ return;
266
+ }
267
+ } else if(!authtoken || authtoken.length === 0 || authtoken !== endpoint.token) {
268
+ res.status(403);
269
+ res.send('Unauthorized');
270
+ return;
271
+ }
272
+ }
273
+ // Make sure Janus is up and running
274
+ if(!this.janus) {
275
+ res.status(503);
276
+ res.send('Janus unavailable');
277
+ return;
278
+ }
279
+ let uuid = this.generateRandomString(16);
280
+ let subscriber = {
281
+ uuid: uuid,
282
+ whepId: id
283
+ };
284
+ this.subscribers.set(uuid, subscriber);
285
+ // Create a new session
286
+ this.logger.info('[' + id + '] Subscribing to WHEP endpoint');
287
+ subscriber.enabling = true;
288
+ try {
289
+ // Connect to the Streaming plugin
290
+ subscriber.handle = await this.janus.attach(StreamingPlugin);
291
+ subscriber.handle.on(Janode.EVENT.HANDLE_DETACHED, () => {
292
+ // Janus notified us the session is gone, tear it down
293
+ let subscriber = this.subscribers.get(uuid);
294
+ if(subscriber) {
295
+ this.logger.info('[' + id + '][' + uuid + '] Handle detached');
296
+ let endpoint = this.endpoints.get(subscriber.whepId);
297
+ if(endpoint)
298
+ endpoint.subscribers.delete(uuid);
299
+ this.subscribers.delete(uuid);
300
+ }
301
+ });
302
+ subscriber.handle.on(Janode.EVENT.HANDLE_HANGUP, async () => {
303
+ // Janus notified us the session is gone, tear it down
304
+ let subscriber = this.subscribers.get(uuid);
305
+ if(subscriber) {
306
+ this.logger.info('[' + id + '][' + uuid + '] PeerConnection closed');
307
+ await subscriber.handle.detach().catch(_err => {});
308
+ if(endpoint) {
309
+ endpoint.subscribers.delete(uuid);
310
+ endpoint.emit('subscriber-gone');
311
+ }
312
+ this.emit('subscriber-gone', id);
313
+ this.subscribers.delete(uuid);
314
+ }
315
+ });
316
+ let details = {
317
+ id: endpoint.mountpoint,
318
+ pin: endpoint.pin
319
+ };
320
+ if(offer) {
321
+ // Client offer (we still support both modes)
322
+ details.jsep = {
323
+ type: 'offer',
324
+ sdp: offer
325
+ };
326
+ }
327
+ const result = await subscriber.handle.watch(details);
328
+ subscriber.enabling = false;
329
+ subscriber.enabled = true;
330
+ endpoint.subscribers.set(uuid, true);
331
+ subscriber.resource = this.config.rest.basePath + '/resource/' + uuid;
332
+ subscriber.latestEtag = this.generateRandomString(16);
333
+ if(offer) {
334
+ subscriber.sdpOffer = offer;
335
+ subscriber.ice = {
336
+ ufrag: subscriber.sdpOffer.match(/a=ice-ufrag:(.*)\r\n/)[1],
337
+ pwd: subscriber.sdpOffer.match(/a=ice-pwd:(.*)\r\n/)[1]
338
+ };
339
+ }
340
+ // Done
341
+ res.setHeader('Access-Control-Expose-Headers', 'Location, Link');
342
+ res.setHeader('Accept-Patch', 'application/trickle-ice-sdpfrag');
343
+ res.setHeader('Location', subscriber.resource);
344
+ res.set('ETag', '"' + subscriber.latestEtag + '"');
345
+ let iceServers = endpoint.iceServers ? endpoint.iceServers : this.config.iceServers;
346
+ if(iceServers && iceServers.length > 0) {
347
+ // Add a Link header for each static ICE server
348
+ let links = [];
349
+ for(let server of iceServers) {
350
+ if(!server.uri || (server.uri.indexOf('stun:') !== 0 &&
351
+ server.uri.indexOf('turn:') !== 0 &&
352
+ server.uri.indexOf('turns:') !== 0))
353
+ continue;
354
+ let link = '<' + server.uri + '>; rel="ice-server"';
355
+ if(server.username && server.credential) {
356
+ link += ';';
357
+ link += ' username="' + server.username + '";' +
358
+ ' credential="' + server.credential + '";' +
359
+ ' credential-type="password"';
360
+ }
361
+ links.push(link);
362
+ }
363
+ res.setHeader('Link', links);
364
+ }
365
+ res.writeHeader(201, { 'Content-Type': 'application/sdp' });
366
+ res.write(result.jsep.sdp);
367
+ res.end();
368
+ endpoint.emit('new-subscriber');
369
+ this.emit('new-subscriber', id);
370
+ } catch(err) {
371
+ this.logger.err('Error subscribing:', err);
372
+ this.subscribers.delete(uuid);
373
+ res.status(500);
374
+ res.send(err.error);
375
+ }
376
+ });
377
+
378
+ // GET, HEAD and PUT on the endpoint must return a 405
379
+ router.get('/endpoint/:id', (_req, res) => {
380
+ res.sendStatus(405);
381
+ });
382
+ router.head('/endpoint/:id', (_req, res) => {
383
+ res.sendStatus(405);
384
+ });
385
+ router.put('/endpoint/:id', (_req, res) => {
386
+ res.sendStatus(405);
387
+ });
388
+
389
+ // Patch can be used both for the SDP answer and to trickle a WHEP resource
390
+ router.patch('/resource/:uuid', async (req, res) => {
391
+ let uuid = req.params.uuid;
392
+ let subscriber = this.subscribers.get(uuid);
393
+ if(subscriber && subscriber.latestEtag)
394
+ res.set('ETag', '"' + subscriber.latestEtag + '"');
395
+ if(!uuid || !subscriber || !subscriber.handle) {
396
+ res.status(404);
397
+ res.send('Invalid resource ID');
398
+ return;
399
+ }
400
+ let endpoint = this.endpoints.get(subscriber.whepId);
401
+ if(!endpoint) {
402
+ res.status(404);
403
+ res.send('Invalid endpoint ID');
404
+ return;
405
+ }
406
+ if(!this.janus) {
407
+ res.status(503);
408
+ res.send('Janus unavailable');
409
+ return;
410
+ }
411
+ if(req.headers['content-type'] === 'application/sdp') {
412
+ // We received an SDP answer from the client
413
+ this.logger.verb('/resource[answer]/:', uuid);
414
+ this.logger.debug(req.body);
415
+ // Prepare the JSEP object
416
+ await subscriber.handle.start({
417
+ jsep: {
418
+ type: 'answer',
419
+ sdp: req.body
420
+ }
421
+ }).catch(err => {
422
+ this.logger.err('Error finalizing subscription:', err);
423
+ let endpoint = this.endpoints.get(subscriber.whepId);
424
+ if(endpoint)
425
+ endpoint.subscribers.delete(uuid);
426
+ this.subscribers.delete(uuid);
427
+ res.status(500);
428
+ res.send(err.error);
429
+ });
430
+ this.logger.info('[' + uuid + '] Completed WHEP negotiation');
431
+ res.sendStatus(204);
432
+ return;
433
+ }
434
+ // If we got here, we're handling a trickle candidate
435
+ this.logger.verb('/resource[trickle]/:', uuid);
436
+ this.logger.debug(req.body);
437
+ // Check the Bearer token
438
+ let auth = req.headers['authorization'];
439
+ if(endpoint.token) {
440
+ if(!auth || auth.indexOf('Bearer ') < 0) {
441
+ res.status(403);
442
+ res.send('Unauthorized');
443
+ return;
444
+ }
445
+ let authtoken = auth.split('Bearer ')[1];
446
+ if(typeof endpoint.token === 'function') {
447
+ if(!endpoint.token(authtoken)) {
448
+ res.status(403);
449
+ res.send('Unauthorized');
450
+ return;
451
+ }
452
+ } else if(!authtoken || authtoken.length === 0 || authtoken !== endpoint.token) {
453
+ res.status(403);
454
+ res.send('Unauthorized');
455
+ return;
456
+ }
457
+ }
458
+ // Check the latest ETag
459
+ if(req.headers['if-match'] !== '"*"' && req.headers['if-match'] !== ('"' + endpoint.latestEtag + '"')) {
460
+ if(this.config.strictETags) {
461
+ // Only return a failure if we're configured with strict ETag checking, ignore it otherwise
462
+ res.status(412);
463
+ res.send('Precondition Failed');
464
+ return;
465
+ }
466
+ }
467
+ // Make sure we received a trickle candidate
468
+ if(req.headers['content-type'] !== 'application/trickle-ice-sdpfrag') {
469
+ res.status(406);
470
+ res.send('Unsupported content type');
471
+ return;
472
+ }
473
+ // Parse the RFC 8840 payload
474
+ let fragment = req.body;
475
+ let lines = fragment.split(/\r?\n/);
476
+ let iceUfrag = null, icePwd = null, restart = false;
477
+ let candidates = [];
478
+ for(let line of lines) {
479
+ if(line.indexOf('a=ice-ufrag:') === 0) {
480
+ iceUfrag = line.split('a=ice-ufrag:')[1];
481
+ } else if(line.indexOf('a=ice-pwd:') === 0) {
482
+ icePwd = line.split('a=ice-pwd:')[1];
483
+ } else if(line.indexOf('a=candidate:') === 0) {
484
+ let candidate = {
485
+ sdpMLineIndex: 0,
486
+ candidate: line.split('a=')[1]
487
+ };
488
+ candidates.push(candidate);
489
+ } else if(line.indexOf('a=end-of-candidates') === 0) {
490
+ // Signal there won't be any more candidates
491
+ candidates.push({ completed: true });
492
+ }
493
+ }
494
+ // Check if there's a restart involved
495
+ if(iceUfrag && icePwd && subscriber.ice && (iceUfrag !== subscriber.ice.ufrag || icePwd !== subscriber.ice.pwd)) {
496
+ // We need to restart
497
+ restart = true;
498
+ }
499
+ // Do one more ETag check (make sure restarts have '*' as ETag, and only them)
500
+ if(req.headers['if-match'] === '*' && this.config.strictETags) {
501
+ // Only return a failure if we're configured with strict ETag checking, ignore it otherwise
502
+ res.status(412);
503
+ res.send('Precondition Failed');
504
+ return;
505
+ }
506
+ try {
507
+ if(!restart) {
508
+ // Trickle the candidate(s)
509
+ if(candidates.length > 0)
510
+ await subscriber.handle.trickle(candidates);
511
+ // We're done
512
+ res.sendStatus(204);
513
+ return;
514
+ }
515
+ // TODO Restarts not supported yet, throw an error
516
+ throw new Error('Restarts not supported yet');
517
+ } catch(err) {
518
+ this.logger.err('Error patching:', err);
519
+ res.status(500);
520
+ res.send(err.error);
521
+ }
522
+ });
523
+
524
+ // Stop subscribing to a WHEP endpoint
525
+ router.delete('/resource/:uuid', async (req, res) => {
526
+ let uuid = req.params.uuid;
527
+ let subscriber = this.subscribers.get(uuid);
528
+ if(subscriber && subscriber.latestEtag)
529
+ res.set('ETag', '"' + subscriber.latestEtag + '"');
530
+ if(!uuid || !subscriber) {
531
+ res.status(404);
532
+ res.send('Invalid resource ID');
533
+ return;
534
+ }
535
+ let endpoint = this.endpoints.get(subscriber.whepId);
536
+ if(!endpoint) {
537
+ res.status(404);
538
+ res.send('Invalid endpoint ID');
539
+ return;
540
+ }
541
+ // Check the Bearer token
542
+ let auth = req.headers['authorization'];
543
+ if(endpoint.token) {
544
+ if(!auth || auth.indexOf('Bearer ') < 0) {
545
+ res.status(403);
546
+ res.send('Unauthorized');
547
+ return;
548
+ }
549
+ let authtoken = auth.split('Bearer ')[1];
550
+ if(typeof endpoint.token === 'function') {
551
+ if(!endpoint.token(authtoken)) {
552
+ res.status(403);
553
+ res.send('Unauthorized');
554
+ return;
555
+ }
556
+ } else if(!authtoken || authtoken.length === 0 || authtoken !== endpoint.token) {
557
+ res.status(403);
558
+ res.send('Unauthorized');
559
+ return;
560
+ }
561
+ }
562
+ this.logger.verb('/resource[delete]/:', uuid);
563
+ // Get rid of the Janus subscriber
564
+ if(this.janus && subscriber.handle)
565
+ await subscriber.handle.detach().catch(_err => {});
566
+ endpoint.subscribers.delete(uuid);
567
+ this.subscribers.delete(uuid);
568
+ this.logger.info('[' + uuid + '] Terminating WHEP session');
569
+ endpoint.emit('subscriber-gone');
570
+ this.emit('subscriber-gone', endpoint.id);
571
+ // Done
572
+ res.sendStatus(200);
573
+ });
574
+
575
+ // GET, HEAD, POST and PUT on the resource must return a 405
576
+ router.get('/resource/:id', (_req, res) => {
577
+ res.sendStatus(405);
578
+ });
579
+ router.head('/resource/:id', (_req, res) => {
580
+ res.sendStatus(405);
581
+ });
582
+ router.post('/resource/:id', (_req, res) => {
583
+ res.sendStatus(405);
584
+ });
585
+ router.put('/resource/:id', (_req, res) => {
586
+ res.sendStatus(405);
587
+ });
588
+
589
+ // Setup CORS
590
+ app.use(cors({ preflightContinue: true }));
591
+
592
+ // Initialize the REST API
593
+ app.use(express.json());
594
+ app.use(express.text({ type: 'application/sdp' }));
595
+ app.use(express.text({ type: 'application/trickle-ice-sdpfrag' }));
596
+ app.use(this.config.rest.basePath, router);
597
+ }
598
+ }
599
+
600
+ // WHEP endpoint class
601
+ class JanusWhepEndpoint extends EventEmitter {
602
+ constructor({ id, mountpoint, pin, label, token, iceServers }) {
603
+ super();
604
+ this.id = id;
605
+ this.mountpoint = mountpoint;
606
+ this.pin = pin;
607
+ this.label = label;
608
+ this.token = token;
609
+ this.iceServers = iceServers;
610
+ // Resources
611
+ this.subscribers = new Map();
612
+ }
613
+
614
+ countSubscribers() {
615
+ return this.subscribers.size;
616
+ }
617
+
618
+ listSubscribers() {
619
+ let list = [];
620
+ this.subscribers.forEach(function(_val, uuid) {
621
+ list.push({ uuid: uuid });
622
+ });
623
+ return list;
624
+
625
+ }
626
+ }
627
+
628
+ // Logger class
629
+ class JanusWhepLogger {
630
+ constructor({ prefix, level }) {
631
+ this.prefix = prefix;
632
+ this.debugLevel = level;
633
+ }
634
+
635
+ err() {
636
+ if(this.debugLevel < 0)
637
+ return;
638
+ let args = Array.prototype.slice.call(arguments);
639
+ args.unshift(this.prefix + '[err]');
640
+ console.log.apply(console, args);
641
+ }
642
+
643
+ warn() {
644
+ if(this.debugLevel < 1)
645
+ return;
646
+ let args = Array.prototype.slice.call(arguments);
647
+ args.unshift(this.prefix + '[warn]');
648
+ console.log.apply(console, args);
649
+ }
650
+
651
+ info() {
652
+ if(this.debugLevel < 2)
653
+ return;
654
+ let args = Array.prototype.slice.call(arguments);
655
+ args.unshift(this.prefix + '[info]');
656
+ console.log.apply(console, args);
657
+ }
658
+
659
+ verb() {
660
+ if(this.debugLevel < 3)
661
+ return;
662
+ let args = Array.prototype.slice.call(arguments);
663
+ args.unshift(this.prefix + '[verb]');
664
+ console.log.apply(console, args);
665
+ }
666
+
667
+ debug() {
668
+ if(this.debugLevel < 4)
669
+ return;
670
+ let args = Array.prototype.slice.call(arguments);
671
+ args.unshift(this.prefix + '[debug]');
672
+ console.log.apply(console, args);
673
+ }
674
+ }
675
+
676
+ // Exports
677
+ export {
678
+ JanusWhepServer,
679
+ JanusWhepEndpoint
680
+ };