@zero-server/sdk 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.
- package/README.md +65 -3
- package/index.js +19 -0
- package/lib/webrtc/cascade.js +577 -0
- package/lib/webrtc/cluster.js +149 -3
- package/lib/webrtc/index.js +73 -11
- package/lib/webrtc/mcu/ffmpeg.js +172 -0
- package/lib/webrtc/mcu/index.js +211 -0
- package/lib/webrtc/observe.js +38 -0
- package/lib/webrtc/recording.js +410 -0
- package/lib/webrtc/room.js +42 -0
- package/lib/webrtc/sfu/index.js +129 -0
- package/lib/webrtc/sfu/livekit.js +304 -0
- package/lib/webrtc/sfu/mediasoup.js +482 -13
- package/lib/webrtc/sfu/memory.js +316 -3
- package/lib/webrtc/signaling.js +175 -1
- package/package.json +1 -1
- package/types/webrtc.d.ts +348 -0
|
@@ -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
|
-
{
|
|
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
|
|
94
|
-
this._transports
|
|
95
|
-
this._producers
|
|
96
|
-
this._consumers
|
|
97
|
-
this.
|
|
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
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
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
|
|