@zero-server/webrtc 0.9.10 → 1.0.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.
@@ -56,9 +56,49 @@
56
56
  const { SfuAdapter } = require('./index');
57
57
  const { WebRTCError } = require('../../errors');
58
58
 
59
+ const DEFAULT_VIDEO_RTCP_FB = [
60
+ { type: 'nack' },
61
+ { type: 'nack', parameter: 'pli' },
62
+ { type: 'ccm', parameter: 'fir' },
63
+ { type: 'goog-remb' },
64
+ { type: 'transport-cc' },
65
+ ];
66
+
59
67
  const DEFAULT_MEDIA_CODECS = [
60
68
  { kind: 'audio', mimeType: 'audio/opus', clockRate: 48000, channels: 2 },
61
- { kind: 'video', mimeType: 'video/VP8', clockRate: 90000 },
69
+ {
70
+ kind: 'video',
71
+ mimeType: 'video/VP8',
72
+ clockRate: 90000,
73
+ rtcpFeedback: DEFAULT_VIDEO_RTCP_FB,
74
+ parameters: { 'x-google-start-bitrate': 1000 },
75
+ },
76
+ {
77
+ kind: 'video',
78
+ mimeType: 'video/VP9',
79
+ clockRate: 90000,
80
+ rtcpFeedback: DEFAULT_VIDEO_RTCP_FB,
81
+ parameters: { 'profile-id': 2, 'x-google-start-bitrate': 1000 },
82
+ },
83
+ {
84
+ kind: 'video',
85
+ mimeType: 'video/H264',
86
+ clockRate: 90000,
87
+ rtcpFeedback: DEFAULT_VIDEO_RTCP_FB,
88
+ parameters: {
89
+ 'packetization-mode': 1,
90
+ 'profile-level-id': '42e01f',
91
+ 'level-asymmetry-allowed': 1,
92
+ 'x-google-start-bitrate': 1000,
93
+ },
94
+ },
95
+ {
96
+ kind: 'video',
97
+ mimeType: 'video/AV1',
98
+ clockRate: 90000,
99
+ rtcpFeedback: DEFAULT_VIDEO_RTCP_FB,
100
+ parameters: { 'profile': 0, 'level-idx': 5, 'tier': 0 },
101
+ },
62
102
  ];
63
103
 
64
104
  const DEFAULT_WEBRTC_TRANSPORT_OPTS = {
@@ -66,6 +106,7 @@ const DEFAULT_WEBRTC_TRANSPORT_OPTS = {
66
106
  enableUdp: true,
67
107
  enableTcp: true,
68
108
  preferUdp: true,
109
+ initialAvailableOutgoingBitrate: 1000000,
69
110
  };
70
111
 
71
112
  class MediasoupSfuAdapter extends SfuAdapter
@@ -77,6 +118,8 @@ class MediasoupSfuAdapter extends SfuAdapter
77
118
  * @param {object} [opts.workerSettings] Forwarded to `mediasoup.createWorker(...)`.
78
119
  * @param {Array} [opts.mediaCodecs] Default router media codecs.
79
120
  * @param {object} [opts.webRtcTransportOptions] Default `router.createWebRtcTransport(...)` options.
121
+ * @param {object} [opts.webRtcServer] Pre-created `WebRtcServer` to share a single UDP/TCP port across workers (k8s / single-IP).
122
+ * @param {object} [opts.webRtcServerOptions] `{ listenInfos: [...] }` forwarded to `worker.createWebRtcServer(...)` on first use.
80
123
  */
81
124
  constructor(opts)
82
125
  {
@@ -89,12 +132,26 @@ class MediasoupSfuAdapter extends SfuAdapter
89
132
  this._webRtcTransportOpts = o.webRtcTransportOptions || DEFAULT_WEBRTC_TRANSPORT_OPTS;
90
133
  this._worker = o.worker || null;
91
134
  this._workerPromise = null;
135
+ this._webRtcServer = o.webRtcServer || null;
136
+ this._webRtcServerOpts = o.webRtcServerOptions || null;
137
+ this._webRtcServerPromise = null;
92
138
 
93
- this._routers = new Map(); // routerId -> native router
94
- this._transports = new Map(); // transportId -> native transport
95
- this._producers = new Map(); // producerId -> native producer
96
- this._consumers = new Map(); // consumerId -> native consumer
97
- this._routerOf = new Map(); // transportId -> routerId
139
+ this._routers = new Map(); // routerId -> native router
140
+ this._transports = new Map(); // transportId -> native transport
141
+ this._producers = new Map(); // producerId -> native producer
142
+ this._consumers = new Map(); // consumerId -> native consumer
143
+ this._dataProducers = new Map(); // dataProducerId -> native dataProducer
144
+ this._dataConsumers = new Map(); // dataConsumerId -> native dataConsumer
145
+ this._observers = new Map(); // observerId -> { id, kind, native, routerId }
146
+ this._pipes = new Map(); // pipeId -> { producerId, pipeProducerId, pipeConsumerId, localRouterId, remoteRouterId }
147
+ this._routerOf = new Map(); // transportId -> routerId
148
+ this._idSeq = 0;
149
+ }
150
+
151
+ _nextId(prefix)
152
+ {
153
+ this._idSeq += 1;
154
+ return `${prefix}-${this._idSeq}`;
98
155
  }
99
156
 
100
157
  /**
@@ -120,9 +177,23 @@ class MediasoupSfuAdapter extends SfuAdapter
120
177
  return this._workerPromise;
121
178
  }
122
179
 
180
+ async _ensureWebRtcServer(worker)
181
+ {
182
+ if (this._webRtcServer) return this._webRtcServer;
183
+ if (!this._webRtcServerOpts) return null;
184
+ if (typeof worker.createWebRtcServer !== 'function') return null;
185
+ if (!this._webRtcServerPromise)
186
+ {
187
+ this._webRtcServerPromise = Promise.resolve(worker.createWebRtcServer(this._webRtcServerOpts))
188
+ .then((s) => { this._webRtcServer = s; return s; });
189
+ }
190
+ return this._webRtcServerPromise;
191
+ }
192
+
123
193
  async createRouter(opts)
124
194
  {
125
195
  const worker = await this._ensureWorker();
196
+ await this._ensureWebRtcServer(worker);
126
197
  const mediaCodecs = (opts && opts.mediaCodecs) || this._mediaCodecs;
127
198
  const router = await worker.createRouter({ mediaCodecs });
128
199
  this._routers.set(router.id, router);
@@ -151,10 +222,16 @@ class MediasoupSfuAdapter extends SfuAdapter
151
222
  {
152
223
  throw new WebRTCError('createTransport: unknown router', { code: 'WEBRTC_SFU_NO_ROUTER' });
153
224
  }
154
- const transport = await native.createWebRtcTransport({
155
- ...this._webRtcTransportOpts,
156
- appData: { peer: peer || null },
157
- });
225
+ const baseOpts = { ...this._webRtcTransportOpts, appData: { peer: peer || null } };
226
+ // If a WebRtcServer is mounted, route the transport through it so
227
+ // every peer shares one UDP/TCP port (single-IP/k8s deployments).
228
+ if (this._webRtcServer && !baseOpts.webRtcServer)
229
+ {
230
+ baseOpts.webRtcServer = this._webRtcServer;
231
+ delete baseOpts.listenIps;
232
+ delete baseOpts.listenInfos;
233
+ }
234
+ const transport = await native.createWebRtcTransport(baseOpts);
158
235
  this._transports.set(transport.id, transport);
159
236
  this._routerOf.set(transport.id, routerId);
160
237
  if (typeof transport.observer === 'object' && transport.observer && typeof transport.observer.on === 'function')
@@ -166,6 +243,13 @@ class MediasoupSfuAdapter extends SfuAdapter
166
243
  this._emit('transport-close', { transportId: transport.id });
167
244
  });
168
245
  }
246
+ if (typeof transport.on === 'function')
247
+ {
248
+ transport.on('icestatechange', (state) =>
249
+ this._emit('transport-ice-state', { transportId: transport.id, state }));
250
+ transport.on('dtlsstatechange', (state) =>
251
+ this._emit('transport-dtls-state', { transportId: transport.id, state }));
252
+ }
169
253
  this._emit('transport-new', { transportId: transport.id, routerId, peerId: peer && peer.id });
170
254
  return {
171
255
  id: transport.id,
@@ -180,7 +264,7 @@ class MediasoupSfuAdapter extends SfuAdapter
180
264
  };
181
265
  }
182
266
 
183
- async produce(transport, kind, rtpParameters)
267
+ async produce(transport, kind, rtpParameters, produceOpts)
184
268
  {
185
269
  if (kind !== 'audio' && kind !== 'video')
186
270
  {
@@ -191,7 +275,18 @@ class MediasoupSfuAdapter extends SfuAdapter
191
275
  {
192
276
  throw new WebRTCError('produce: unknown transport', { code: 'WEBRTC_SFU_NO_TRANSPORT' });
193
277
  }
194
- const producer = await native.produce({ kind, rtpParameters });
278
+ // Honor caller-supplied simulcast / SVC config. Either pass a
279
+ // 4th arg `{ encodings, keyFrameRequestDelay, paused }` or stash
280
+ // it on `rtpParameters.encodings` directly.
281
+ const o = produceOpts || {};
282
+ const params = { ...rtpParameters };
283
+ if (Array.isArray(o.encodings) && !params.encodings)
284
+ params.encodings = o.encodings;
285
+ const produceArgs = { kind, rtpParameters: params };
286
+ if (o.keyFrameRequestDelay) produceArgs.keyFrameRequestDelay = o.keyFrameRequestDelay;
287
+ if (typeof o.paused === 'boolean') produceArgs.paused = o.paused;
288
+ if (o.appData) produceArgs.appData = o.appData;
289
+ const producer = await native.produce(produceArgs);
195
290
  this._producers.set(producer.id, producer);
196
291
  if (typeof producer.on === 'function')
197
292
  {
@@ -200,6 +295,12 @@ class MediasoupSfuAdapter extends SfuAdapter
200
295
  this._producers.delete(producer.id);
201
296
  this._emit('producer-close', { producerId: producer.id, reason: 'transport-close' });
202
297
  });
298
+ producer.on('score', (score) =>
299
+ this._emit('producer-score', { producerId: producer.id, score }));
300
+ producer.on('videoorientationchange', (orientation) =>
301
+ this._emit('producer-orientation', { producerId: producer.id, orientation }));
302
+ producer.on('trace', (trace) =>
303
+ this._emit('producer-trace', { producerId: producer.id, trace }));
203
304
  }
204
305
  this._emit('producer-new', { producerId: producer.id, transportId: transport.id, kind });
205
306
  return {
@@ -207,7 +308,7 @@ class MediasoupSfuAdapter extends SfuAdapter
207
308
  producerId: producer.id,
208
309
  transportId: transport.id,
209
310
  kind,
210
- rtpParameters,
311
+ rtpParameters: params,
211
312
  paused: !!producer.paused,
212
313
  _native: producer,
213
314
  };
@@ -250,6 +351,11 @@ class MediasoupSfuAdapter extends SfuAdapter
250
351
  this._consumers.delete(consumer.id);
251
352
  this._emit('consumer-close', { consumerId: consumer.id, reason: 'producer-close' });
252
353
  });
354
+ consumer.on('producerpause', () => this._emit('consumer-producer-pause', { consumerId: consumer.id }));
355
+ consumer.on('producerresume', () => this._emit('consumer-producer-resume', { consumerId: consumer.id }));
356
+ consumer.on('score', (score) => this._emit('consumer-score', { consumerId: consumer.id, score }));
357
+ consumer.on('layerschange', (layers) => this._emit('consumer-layers-change', { consumerId: consumer.id, layers }));
358
+ consumer.on('trace', (trace) => this._emit('consumer-trace', { consumerId: consumer.id, trace }));
253
359
  }
254
360
  this._emit('consumer-new', { consumerId: consumer.id, transportId: transport.id, producerId });
255
361
  return {
@@ -329,12 +435,375 @@ class MediasoupSfuAdapter extends SfuAdapter
329
435
  {
330
436
  try { await this.closeRouter(id); } catch (_) { /* swallow */ }
331
437
  }
438
+ if (this._webRtcServer && typeof this._webRtcServer.close === 'function')
439
+ {
440
+ try { await this._webRtcServer.close(); } catch (_) { /* swallow */ }
441
+ }
332
442
  if (this._worker && typeof this._worker.close === 'function')
333
443
  {
334
444
  try { await this._worker.close(); } catch (_) { /* swallow */ }
335
445
  }
336
446
  this._worker = null;
337
447
  this._workerPromise = null;
448
+ this._webRtcServer = null;
449
+ this._webRtcServerPromise = null;
450
+ }
451
+
452
+ // ----- Consumer-side BWE / quality controls -----
453
+
454
+ async setConsumerPreferredLayers(consumerId, layers)
455
+ {
456
+ const c = this._consumers.get(consumerId);
457
+ if (!c)
458
+ {
459
+ throw new WebRTCError('setConsumerPreferredLayers: unknown consumer', { code: 'WEBRTC_SFU_NO_CONSUMER' });
460
+ }
461
+ if (!layers || typeof layers.spatialLayer !== 'number')
462
+ {
463
+ throw new WebRTCError('setConsumerPreferredLayers: layers.spatialLayer must be a number', { code: 'WEBRTC_SFU_INVALID_LAYERS' });
464
+ }
465
+ await c.setPreferredLayers({
466
+ spatialLayer: layers.spatialLayer,
467
+ temporalLayer: typeof layers.temporalLayer === 'number' ? layers.temporalLayer : undefined,
468
+ });
469
+ }
470
+
471
+ async setConsumerPriority(consumerId, priority)
472
+ {
473
+ const c = this._consumers.get(consumerId);
474
+ if (!c)
475
+ {
476
+ throw new WebRTCError('setConsumerPriority: unknown consumer', { code: 'WEBRTC_SFU_NO_CONSUMER' });
477
+ }
478
+ const p = Number(priority);
479
+ if (!Number.isFinite(p) || p < 1 || p > 255)
480
+ {
481
+ throw new WebRTCError('setConsumerPriority: priority must be 1..255', { code: 'WEBRTC_SFU_INVALID_PRIORITY' });
482
+ }
483
+ await c.setPriority(p);
484
+ }
485
+
486
+ async requestKeyFrame(consumerId)
487
+ {
488
+ const c = this._consumers.get(consumerId);
489
+ if (!c)
490
+ {
491
+ throw new WebRTCError('requestKeyFrame: unknown consumer', { code: 'WEBRTC_SFU_NO_CONSUMER' });
492
+ }
493
+ if (typeof c.requestKeyFrame === 'function') await c.requestKeyFrame();
494
+ }
495
+
496
+ async pauseConsumer(consumerId)
497
+ {
498
+ const c = this._consumers.get(consumerId);
499
+ if (!c)
500
+ {
501
+ throw new WebRTCError('pauseConsumer: unknown consumer', { code: 'WEBRTC_SFU_NO_CONSUMER' });
502
+ }
503
+ await c.pause();
504
+ this._emit('consumer-pause', { consumerId });
505
+ }
506
+
507
+ async resumeConsumer(consumerId)
508
+ {
509
+ const c = this._consumers.get(consumerId);
510
+ if (!c)
511
+ {
512
+ throw new WebRTCError('resumeConsumer: unknown consumer', { code: 'WEBRTC_SFU_NO_CONSUMER' });
513
+ }
514
+ await c.resume();
515
+ this._emit('consumer-resume', { consumerId });
516
+ }
517
+
518
+ // ----- Transport bitrate clamps -----
519
+
520
+ async setTransportBitrates(transportId, opts)
521
+ {
522
+ const t = this._transports.get(transportId);
523
+ if (!t)
524
+ {
525
+ throw new WebRTCError('setTransportBitrates: unknown transport', { code: 'WEBRTC_SFU_NO_TRANSPORT' });
526
+ }
527
+ const o = opts || {};
528
+ if (Number.isFinite(o.maxIncoming) && typeof t.setMaxIncomingBitrate === 'function')
529
+ await t.setMaxIncomingBitrate(o.maxIncoming);
530
+ if (Number.isFinite(o.maxOutgoing) && typeof t.setMaxOutgoingBitrate === 'function')
531
+ await t.setMaxOutgoingBitrate(o.maxOutgoing);
532
+ if (Number.isFinite(o.min) && typeof t.setMinOutgoingBitrate === 'function')
533
+ await t.setMinOutgoingBitrate(o.min);
534
+ this._emit('transport-bitrates', { transportId, bitrates: o });
535
+ }
536
+
537
+ // ----- SCTP data channels -----
538
+
539
+ async produceData(transport, opts)
540
+ {
541
+ const native = transport && this._transports.get(transport.id);
542
+ if (!native)
543
+ {
544
+ throw new WebRTCError('produceData: unknown transport', { code: 'WEBRTC_SFU_NO_TRANSPORT' });
545
+ }
546
+ const o = opts || {};
547
+ const dp = await native.produceData({
548
+ label: o.label || '',
549
+ protocol: o.protocol || '',
550
+ ordered: o.ordered !== false,
551
+ sctpStreamParameters: o.sctpStreamParameters,
552
+ });
553
+ this._dataProducers.set(dp.id, dp);
554
+ if (typeof dp.on === 'function')
555
+ {
556
+ dp.on('transportclose', () =>
557
+ {
558
+ this._dataProducers.delete(dp.id);
559
+ this._emit('data-producer-close', { dataProducerId: dp.id });
560
+ });
561
+ }
562
+ this._emit('data-producer-new', { dataProducerId: dp.id, transportId: transport.id, label: o.label || '' });
563
+ return {
564
+ id: dp.id,
565
+ dataProducerId: dp.id,
566
+ transportId: transport.id,
567
+ label: dp.label || o.label || '',
568
+ protocol: dp.protocol || o.protocol || '',
569
+ ordered: dp.sctpStreamParameters ? !!dp.sctpStreamParameters.ordered : (o.ordered !== false),
570
+ _native: dp,
571
+ };
572
+ }
573
+
574
+ async consumeData(transport, dataProducerId, opts)
575
+ {
576
+ const native = transport && this._transports.get(transport.id);
577
+ if (!native)
578
+ {
579
+ throw new WebRTCError('consumeData: unknown transport', { code: 'WEBRTC_SFU_NO_TRANSPORT' });
580
+ }
581
+ const o = opts || {};
582
+ const dc = await native.consumeData({
583
+ dataProducerId,
584
+ ordered: o.ordered,
585
+ });
586
+ this._dataConsumers.set(dc.id, dc);
587
+ if (typeof dc.on === 'function')
588
+ {
589
+ dc.on('transportclose', () =>
590
+ {
591
+ this._dataConsumers.delete(dc.id);
592
+ this._emit('data-consumer-close', { dataConsumerId: dc.id });
593
+ });
594
+ dc.on('dataproducerclose', () =>
595
+ {
596
+ this._dataConsumers.delete(dc.id);
597
+ this._emit('data-consumer-close', { dataConsumerId: dc.id, reason: 'data-producer-close' });
598
+ });
599
+ }
600
+ this._emit('data-consumer-new', { dataConsumerId: dc.id, transportId: transport.id, dataProducerId });
601
+ return {
602
+ id: dc.id,
603
+ dataConsumerId: dc.id,
604
+ transportId: transport.id,
605
+ dataProducerId,
606
+ label: dc.label || '',
607
+ protocol: dc.protocol || '',
608
+ ordered: dc.sctpStreamParameters ? !!dc.sctpStreamParameters.ordered : true,
609
+ _native: dc,
610
+ };
611
+ }
612
+
613
+ // ----- Observers -----
614
+
615
+ async observeAudioLevels(routerId, opts)
616
+ {
617
+ const r = this._routers.get(routerId);
618
+ if (!r)
619
+ {
620
+ throw new WebRTCError('observeAudioLevels: unknown router', { code: 'WEBRTC_SFU_NO_ROUTER' });
621
+ }
622
+ const o = opts || {};
623
+ const native = await r.createAudioLevelObserver({
624
+ interval: o.interval || 1000,
625
+ threshold: o.threshold || -80,
626
+ maxEntries: o.maxEntries || 1,
627
+ });
628
+ const id = this._nextId('audioObserver');
629
+ const handle = {
630
+ id,
631
+ kind: 'audio-level',
632
+ routerId,
633
+ _native: native,
634
+ close: async () =>
635
+ {
636
+ if (handle.closed) return;
637
+ handle.closed = true;
638
+ if (typeof native.close === 'function') try { await native.close(); } catch (_) { /* swallow */ }
639
+ this._observers.delete(id);
640
+ this._emit('observer-close', { observerId: id, kind: 'audio-level' });
641
+ },
642
+ emit: (levels) =>
643
+ {
644
+ if (handle.closed) return;
645
+ this._emit('audio-level', { observerId: id, routerId, levels });
646
+ },
647
+ closed: false,
648
+ };
649
+ if (typeof native.on === 'function')
650
+ {
651
+ native.on('volumes', (volumes) =>
652
+ this._emit('audio-level', { observerId: id, routerId, levels: volumes }));
653
+ native.on('silence', () =>
654
+ this._emit('audio-silence', { observerId: id, routerId }));
655
+ }
656
+ this._observers.set(id, handle);
657
+ this._emit('observer-new', { observerId: id, kind: 'audio-level', routerId });
658
+ return handle;
659
+ }
660
+
661
+ async observeActiveSpeaker(routerId, opts)
662
+ {
663
+ const r = this._routers.get(routerId);
664
+ if (!r)
665
+ {
666
+ throw new WebRTCError('observeActiveSpeaker: unknown router', { code: 'WEBRTC_SFU_NO_ROUTER' });
667
+ }
668
+ const o = opts || {};
669
+ const native = await r.createActiveSpeakerObserver({
670
+ interval: o.interval || 300,
671
+ });
672
+ const id = this._nextId('speakerObserver');
673
+ const handle = {
674
+ id,
675
+ kind: 'active-speaker',
676
+ routerId,
677
+ _native: native,
678
+ close: async () =>
679
+ {
680
+ if (handle.closed) return;
681
+ handle.closed = true;
682
+ if (typeof native.close === 'function') try { await native.close(); } catch (_) { /* swallow */ }
683
+ this._observers.delete(id);
684
+ this._emit('observer-close', { observerId: id, kind: 'active-speaker' });
685
+ },
686
+ emit: (producerId) =>
687
+ {
688
+ if (handle.closed) return;
689
+ this._emit('active-speaker', { observerId: id, routerId, producerId });
690
+ },
691
+ closed: false,
692
+ };
693
+ if (typeof native.on === 'function')
694
+ {
695
+ native.on('dominantspeaker', (ev) =>
696
+ this._emit('active-speaker', { observerId: id, routerId, producerId: ev && ev.producer && ev.producer.id }));
697
+ }
698
+ this._observers.set(id, handle);
699
+ this._emit('observer-new', { observerId: id, kind: 'active-speaker', routerId });
700
+ return handle;
701
+ }
702
+
703
+ // ----- Cross-router pipe (cascade hop / same-host fanout) -----
704
+
705
+ async pipeToRouter(opts)
706
+ {
707
+ const o = opts || {};
708
+ const prod = this._producers.get(o.producerId);
709
+ if (!prod)
710
+ {
711
+ throw new WebRTCError('pipeToRouter: unknown producer', { code: 'WEBRTC_SFU_NO_PRODUCER' });
712
+ }
713
+ const local = this._routers.get(o.localRouterId);
714
+ if (!local)
715
+ {
716
+ throw new WebRTCError('pipeToRouter: unknown local router', { code: 'WEBRTC_SFU_NO_ROUTER' });
717
+ }
718
+ if (!o.remoteRouter)
719
+ {
720
+ throw new WebRTCError('pipeToRouter: opts.remoteRouter is required', { code: 'WEBRTC_SFU_INVALID_PIPE' });
721
+ }
722
+ // mediasoup supports both `router.pipeToRouter()` (native, in-process) and the
723
+ // manual PipeTransport handshake for cross-process / cross-host pipes.
724
+ const remoteNative = o.remoteRouter._native || o.remoteRouter;
725
+ const result = await local.pipeToRouter({
726
+ producerId: o.producerId,
727
+ router: remoteNative,
728
+ listenInfo: o.listenInfo,
729
+ enableSrtp: o.enableSrtp,
730
+ });
731
+ const id = this._nextId('pipe');
732
+ const handle = {
733
+ id,
734
+ pipeId: id,
735
+ producerId: o.producerId,
736
+ localRouterId: o.localRouterId,
737
+ remoteRouterId: o.remoteRouter.id || o.remoteRouter.routerId,
738
+ pipeProducerId: result && result.pipeProducer && result.pipeProducer.id,
739
+ pipeConsumerId: result && result.pipeConsumer && result.pipeConsumer.id,
740
+ _native: result,
741
+ };
742
+ this._pipes.set(id, handle);
743
+ this._emit('pipe-open', handle);
744
+ return handle;
745
+ }
746
+
747
+ // ----- Per-entity stats -----
748
+
749
+ async getProducerStats(producerId)
750
+ {
751
+ const p = this._producers.get(producerId);
752
+ if (!p)
753
+ {
754
+ throw new WebRTCError('getProducerStats: unknown producer', { code: 'WEBRTC_SFU_NO_PRODUCER' });
755
+ }
756
+ return typeof p.getStats === 'function' ? p.getStats() : [];
757
+ }
758
+
759
+ async getConsumerStats(consumerId)
760
+ {
761
+ const c = this._consumers.get(consumerId);
762
+ if (!c)
763
+ {
764
+ throw new WebRTCError('getConsumerStats: unknown consumer', { code: 'WEBRTC_SFU_NO_CONSUMER' });
765
+ }
766
+ return typeof c.getStats === 'function' ? c.getStats() : [];
767
+ }
768
+
769
+ async getTransportStats(transportId)
770
+ {
771
+ const t = this._transports.get(transportId);
772
+ if (!t)
773
+ {
774
+ throw new WebRTCError('getTransportStats: unknown transport', { code: 'WEBRTC_SFU_NO_TRANSPORT' });
775
+ }
776
+ return typeof t.getStats === 'function' ? t.getStats() : [];
777
+ }
778
+
779
+ // ----- RTP/RTCP trace -----
780
+
781
+ async enableTraceEvent(routerId, types)
782
+ {
783
+ const list = Array.isArray(types) ? types : [];
784
+ // mediasoup trace events live on transport/producer/consumer, not router.
785
+ // Apply to every entity bound to the given router so a caller can
786
+ // flip trace at a coarse "this room please" granularity.
787
+ const targets = [];
788
+ for (const [tid, t] of this._transports)
789
+ {
790
+ if (this._routerOf.get(tid) === routerId && typeof t.enableTraceEvent === 'function')
791
+ targets.push(t);
792
+ }
793
+ for (const p of this._producers.values())
794
+ {
795
+ if (typeof p.enableTraceEvent === 'function'
796
+ && this._routerOf.get(p.transportId || (p._appData && p._appData.transportId)) === routerId)
797
+ targets.push(p);
798
+ }
799
+ for (const c of this._consumers.values())
800
+ {
801
+ if (typeof c.enableTraceEvent === 'function'
802
+ && this._routerOf.get(c.transportId || (c._appData && c._appData.transportId)) === routerId)
803
+ targets.push(c);
804
+ }
805
+ await Promise.all(targets.map((x) => x.enableTraceEvent(list)));
806
+ this._emit('trace-enabled', { routerId, types: list });
338
807
  }
339
808
  }
340
809