@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
package/lib/webrtc/room.js
CHANGED
|
@@ -72,6 +72,23 @@ class Room
|
|
|
72
72
|
|
|
73
73
|
/** @type {boolean} `true` once `.open()` has been called. */
|
|
74
74
|
this.isOpen = false;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Currently advertised topology for this room. `mesh` until the
|
|
78
|
+
* hub promotes it (in `auto` mode) or until the application calls
|
|
79
|
+
* `setTopology()`. Broadcast to peers via `room-topology` so client
|
|
80
|
+
* SDKs can switch between p2p / SFU transports without a rejoin.
|
|
81
|
+
* @type {'mesh'|'sfu'|'mcu'}
|
|
82
|
+
*/
|
|
83
|
+
this.topology = 'mesh';
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Original topology preference inherited from the hub at room
|
|
87
|
+
* creation time. `auto` means the hub may promote/demote between
|
|
88
|
+
* `mesh` and `sfu` based on `maxMeshPeers`.
|
|
89
|
+
* @type {'mesh'|'sfu'|'mcu'|'auto'}
|
|
90
|
+
*/
|
|
91
|
+
this.topologyMode = 'auto';
|
|
75
92
|
}
|
|
76
93
|
|
|
77
94
|
// -- Configuration (fluent) --
|
|
@@ -139,6 +156,8 @@ class Room
|
|
|
139
156
|
{
|
|
140
157
|
this._peers.add(peer);
|
|
141
158
|
peer.room = this;
|
|
159
|
+
if (this.hub && typeof this.hub._onRoomMembershipChange === 'function')
|
|
160
|
+
this.hub._onRoomMembershipChange(this, 'add', peer);
|
|
142
161
|
}
|
|
143
162
|
|
|
144
163
|
/** Internal - hub uses this; do not call from application code. */
|
|
@@ -147,6 +166,29 @@ class Room
|
|
|
147
166
|
if (!this._peers.has(peer)) return;
|
|
148
167
|
this._peers.delete(peer);
|
|
149
168
|
if (peer.room === this) peer.room = null;
|
|
169
|
+
if (this.hub && typeof this.hub._onRoomMembershipChange === 'function')
|
|
170
|
+
this.hub._onRoomMembershipChange(this, 'remove', peer);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Force the room's topology and broadcast the change to every peer.
|
|
175
|
+
* Pure signaling: the hub does not redirect media; client SDKs are
|
|
176
|
+
* responsible for switching transports when the new topology is
|
|
177
|
+
* received via `room-topology`.
|
|
178
|
+
* @param {'mesh'|'sfu'|'mcu'} topology
|
|
179
|
+
*/
|
|
180
|
+
setTopology(topology)
|
|
181
|
+
{
|
|
182
|
+
if (topology !== 'mesh' && topology !== 'sfu' && topology !== 'mcu')
|
|
183
|
+
throw new SignalingError('Room.setTopology: topology must be mesh|sfu|mcu');
|
|
184
|
+
if (this.topology === topology) return this;
|
|
185
|
+
const previous = this.topology;
|
|
186
|
+
this.topology = topology;
|
|
187
|
+
for (const p of this._peers)
|
|
188
|
+
p.send('room-topology', { room: this.name, topology, previous });
|
|
189
|
+
if (this.hub)
|
|
190
|
+
this.hub.emit('topology:changed', { room: this, topology, previous });
|
|
191
|
+
return this;
|
|
150
192
|
}
|
|
151
193
|
|
|
152
194
|
// -- Fan-out --
|
package/lib/webrtc/sfu/index.js
CHANGED
|
@@ -116,6 +116,135 @@ class SfuAdapter
|
|
|
116
116
|
throw new WebRTCError('SfuAdapter.stats() not implemented', { code: 'WEBRTC_SFU_NOT_IMPLEMENTED' });
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
// -- Consumer-side BWE / quality knobs --
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Switch a consumer to the given simulcast spatial / temporal layer.
|
|
123
|
+
* @param {string} consumerId
|
|
124
|
+
* @param {{spatialLayer:number, temporalLayer?:number}} layers
|
|
125
|
+
*/
|
|
126
|
+
async setConsumerPreferredLayers(_consumerId, _layers)
|
|
127
|
+
{
|
|
128
|
+
throw new WebRTCError('SfuAdapter.setConsumerPreferredLayers() not implemented', { code: 'WEBRTC_SFU_NOT_IMPLEMENTED' });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Set consumer priority (1-255, higher = more bandwidth budget under
|
|
133
|
+
* congestion).
|
|
134
|
+
*/
|
|
135
|
+
async setConsumerPriority(_consumerId, _priority)
|
|
136
|
+
{
|
|
137
|
+
throw new WebRTCError('SfuAdapter.setConsumerPriority() not implemented', { code: 'WEBRTC_SFU_NOT_IMPLEMENTED' });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Ask the SFU to forward a PLI/FIR to the producer the consumer is bound to. */
|
|
141
|
+
async requestKeyFrame(_consumerId)
|
|
142
|
+
{
|
|
143
|
+
throw new WebRTCError('SfuAdapter.requestKeyFrame() not implemented', { code: 'WEBRTC_SFU_NOT_IMPLEMENTED' });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Pause an individual consumer (stop forwarding to a single subscriber). */
|
|
147
|
+
async pauseConsumer(_consumerId)
|
|
148
|
+
{
|
|
149
|
+
throw new WebRTCError('SfuAdapter.pauseConsumer() not implemented', { code: 'WEBRTC_SFU_NOT_IMPLEMENTED' });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Resume a previously paused consumer. */
|
|
153
|
+
async resumeConsumer(_consumerId)
|
|
154
|
+
{
|
|
155
|
+
throw new WebRTCError('SfuAdapter.resumeConsumer() not implemented', { code: 'WEBRTC_SFU_NOT_IMPLEMENTED' });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// -- Transport-level BWE caps --
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Apply incoming/outgoing bitrate hints to a transport.
|
|
162
|
+
* @param {string} transportId
|
|
163
|
+
* @param {{initial?:number, min?:number, max?:number, maxIncoming?:number, maxOutgoing?:number}} opts
|
|
164
|
+
*/
|
|
165
|
+
async setTransportBitrates(_transportId, _opts)
|
|
166
|
+
{
|
|
167
|
+
throw new WebRTCError('SfuAdapter.setTransportBitrates() not implemented', { code: 'WEBRTC_SFU_NOT_IMPLEMENTED' });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// -- Data channels --
|
|
171
|
+
|
|
172
|
+
/** Create an SCTP data producer on a transport. */
|
|
173
|
+
async produceData(_transport, _opts)
|
|
174
|
+
{
|
|
175
|
+
throw new WebRTCError('SfuAdapter.produceData() not implemented', { code: 'WEBRTC_SFU_NOT_IMPLEMENTED' });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Create an SCTP data consumer bound to `dataProducerId`. */
|
|
179
|
+
async consumeData(_transport, _dataProducerId, _opts)
|
|
180
|
+
{
|
|
181
|
+
throw new WebRTCError('SfuAdapter.consumeData() not implemented', { code: 'WEBRTC_SFU_NOT_IMPLEMENTED' });
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// -- Observers --
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Start an audio-level observer on a router. Emits `audio-level` adapter
|
|
188
|
+
* events. Returns `{ id, close() }`.
|
|
189
|
+
* @param {string} routerId
|
|
190
|
+
* @param {{interval?:number, threshold?:number, maxEntries?:number}} [opts]
|
|
191
|
+
*/
|
|
192
|
+
async observeAudioLevels(_routerId, _opts)
|
|
193
|
+
{
|
|
194
|
+
throw new WebRTCError('SfuAdapter.observeAudioLevels() not implemented', { code: 'WEBRTC_SFU_NOT_IMPLEMENTED' });
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Start an active-speaker observer. Emits `active-speaker` adapter
|
|
199
|
+
* events. Returns `{ id, close() }`.
|
|
200
|
+
*/
|
|
201
|
+
async observeActiveSpeaker(_routerId, _opts)
|
|
202
|
+
{
|
|
203
|
+
throw new WebRTCError('SfuAdapter.observeActiveSpeaker() not implemented', { code: 'WEBRTC_SFU_NOT_IMPLEMENTED' });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// -- Cross-router cascade (SFU mesh) --
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Pipe a producer from this adapter's local router into another router
|
|
210
|
+
* (possibly on a remote SFU node). Returns
|
|
211
|
+
* `{ pipeProducerId, pipeConsumerId, localRouterId, remoteRouterId }`.
|
|
212
|
+
* @param {{producerId:string, localRouterId:string, remoteRouter:object}} opts
|
|
213
|
+
*/
|
|
214
|
+
async pipeToRouter(_opts)
|
|
215
|
+
{
|
|
216
|
+
throw new WebRTCError('SfuAdapter.pipeToRouter() not implemented', { code: 'WEBRTC_SFU_NOT_IMPLEMENTED' });
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// -- Targeted stats (Phase 2 surface; coarse stats() remains for back-compat) --
|
|
220
|
+
|
|
221
|
+
/** Return native stats for a single producer. */
|
|
222
|
+
async getProducerStats(_producerId)
|
|
223
|
+
{
|
|
224
|
+
throw new WebRTCError('SfuAdapter.getProducerStats() not implemented', { code: 'WEBRTC_SFU_NOT_IMPLEMENTED' });
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Return native stats for a single consumer. */
|
|
228
|
+
async getConsumerStats(_consumerId)
|
|
229
|
+
{
|
|
230
|
+
throw new WebRTCError('SfuAdapter.getConsumerStats() not implemented', { code: 'WEBRTC_SFU_NOT_IMPLEMENTED' });
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Return native stats for a single transport. */
|
|
234
|
+
async getTransportStats(_transportId)
|
|
235
|
+
{
|
|
236
|
+
throw new WebRTCError('SfuAdapter.getTransportStats() not implemented', { code: 'WEBRTC_SFU_NOT_IMPLEMENTED' });
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Toggle low-level trace event emission on a router (mediasoup `trace`
|
|
241
|
+
* events: 'probation', 'bwe', 'rtp', 'keyframe', etc.).
|
|
242
|
+
*/
|
|
243
|
+
async enableTraceEvent(_routerId, _types)
|
|
244
|
+
{
|
|
245
|
+
throw new WebRTCError('SfuAdapter.enableTraceEvent() not implemented', { code: 'WEBRTC_SFU_NOT_IMPLEMENTED' });
|
|
246
|
+
}
|
|
247
|
+
|
|
119
248
|
/**
|
|
120
249
|
* Register a handler invoked as `(event, payload)` for adapter-level
|
|
121
250
|
* events ('producer-new', 'producer-pause', 'consumer-new',
|
|
@@ -283,6 +283,310 @@ class LiveKitSfuAdapter extends SfuAdapter
|
|
|
283
283
|
rooms,
|
|
284
284
|
};
|
|
285
285
|
}
|
|
286
|
+
|
|
287
|
+
// ----- Room / participant REST passthroughs -----
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Fetch one room from LiveKit by name. Returns the matching entry
|
|
291
|
+
* from `client.listRooms([name])` or `null`.
|
|
292
|
+
*/
|
|
293
|
+
async getRoomInfo(routerId)
|
|
294
|
+
{
|
|
295
|
+
if (typeof this._client.listRooms !== 'function') return null;
|
|
296
|
+
try
|
|
297
|
+
{
|
|
298
|
+
const list = await this._client.listRooms([routerId]);
|
|
299
|
+
if (Array.isArray(list) && list.length) return list[0];
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
catch (_) { return null; }
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async listParticipants(routerId)
|
|
306
|
+
{
|
|
307
|
+
if (typeof this._client.listParticipants !== 'function') return [];
|
|
308
|
+
return this._client.listParticipants(routerId);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async removeParticipant(routerId, identity)
|
|
312
|
+
{
|
|
313
|
+
if (typeof this._client.removeParticipant !== 'function')
|
|
314
|
+
{
|
|
315
|
+
throw new WebRTCError('removeParticipant: LiveKit client missing removeParticipant()', { code: 'WEBRTC_SFU_NOT_SUPPORTED' });
|
|
316
|
+
}
|
|
317
|
+
await this._client.removeParticipant(routerId, identity);
|
|
318
|
+
this._emit('peer-removed', { routerId, identity });
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async updateRoomMetadata(routerId, metadata)
|
|
322
|
+
{
|
|
323
|
+
if (typeof this._client.updateRoomMetadata !== 'function')
|
|
324
|
+
{
|
|
325
|
+
throw new WebRTCError('updateRoomMetadata: LiveKit client missing updateRoomMetadata()', { code: 'WEBRTC_SFU_NOT_SUPPORTED' });
|
|
326
|
+
}
|
|
327
|
+
await this._client.updateRoomMetadata(routerId, typeof metadata === 'string' ? metadata : JSON.stringify(metadata));
|
|
328
|
+
this._emit('router-metadata', { routerId, metadata });
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async sendData(routerId, payload, opts)
|
|
332
|
+
{
|
|
333
|
+
if (typeof this._client.sendData !== 'function')
|
|
334
|
+
{
|
|
335
|
+
throw new WebRTCError('sendData: LiveKit client missing sendData()', { code: 'WEBRTC_SFU_NOT_SUPPORTED' });
|
|
336
|
+
}
|
|
337
|
+
const o = opts || {};
|
|
338
|
+
const data = Buffer.isBuffer(payload) ? payload
|
|
339
|
+
: (payload instanceof Uint8Array ? Buffer.from(payload)
|
|
340
|
+
: Buffer.from(typeof payload === 'string' ? payload : JSON.stringify(payload)));
|
|
341
|
+
await this._client.sendData(routerId, data, o.kind || 0, o.destinationIdentities || o.destinations || undefined);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ----- Egress (recording / RTMP / HLS) -----
|
|
345
|
+
|
|
346
|
+
_egressClient()
|
|
347
|
+
{
|
|
348
|
+
if (this._egress) return this._egress;
|
|
349
|
+
if (!this._livekit.EgressClient)
|
|
350
|
+
{
|
|
351
|
+
try { this._livekit = { ...this._livekit, ...require('livekit-server-sdk') }; }
|
|
352
|
+
catch (_) { /* fall through */ }
|
|
353
|
+
}
|
|
354
|
+
const C = this._livekit.EgressClient;
|
|
355
|
+
if (!C)
|
|
356
|
+
{
|
|
357
|
+
throw new WebRTCError(
|
|
358
|
+
"egress requires 'livekit-server-sdk' with EgressClient support",
|
|
359
|
+
{ code: 'WEBRTC_SFU_NOT_INSTALLED' },
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
this._egress = new C(this._url, this._apiKey, this._apiSecret);
|
|
363
|
+
return this._egress;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async startRoomCompositeEgress(routerId, opts)
|
|
367
|
+
{
|
|
368
|
+
const e = this._egressClient();
|
|
369
|
+
const o = opts || {};
|
|
370
|
+
const res = await e.startRoomCompositeEgress(routerId, o.output || o, o.options);
|
|
371
|
+
this._emit('egress-start', { kind: 'room-composite', routerId, egressId: res && res.egressId });
|
|
372
|
+
return res;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async startTrackEgress(routerId, trackId, opts)
|
|
376
|
+
{
|
|
377
|
+
const e = this._egressClient();
|
|
378
|
+
const o = opts || {};
|
|
379
|
+
const res = await e.startTrackEgress(routerId, o.output || o, trackId);
|
|
380
|
+
this._emit('egress-start', { kind: 'track', routerId, trackId, egressId: res && res.egressId });
|
|
381
|
+
return res;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async stopEgress(egressId)
|
|
385
|
+
{
|
|
386
|
+
const e = this._egressClient();
|
|
387
|
+
const res = await e.stopEgress(egressId);
|
|
388
|
+
this._emit('egress-stop', { egressId });
|
|
389
|
+
return res;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async listEgress(opts)
|
|
393
|
+
{
|
|
394
|
+
const e = this._egressClient();
|
|
395
|
+
if (typeof e.listEgress !== 'function') return [];
|
|
396
|
+
return e.listEgress(opts || {});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ----- Ingress (WHIP / RTMP / URL pull) -----
|
|
400
|
+
|
|
401
|
+
_ingressClient()
|
|
402
|
+
{
|
|
403
|
+
if (this._ingress) return this._ingress;
|
|
404
|
+
const C = this._livekit.IngressClient;
|
|
405
|
+
if (!C)
|
|
406
|
+
{
|
|
407
|
+
throw new WebRTCError(
|
|
408
|
+
"ingress requires 'livekit-server-sdk' with IngressClient support",
|
|
409
|
+
{ code: 'WEBRTC_SFU_NOT_INSTALLED' },
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
this._ingress = new C(this._url, this._apiKey, this._apiSecret);
|
|
413
|
+
return this._ingress;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
async createIngress(opts)
|
|
417
|
+
{
|
|
418
|
+
const i = this._ingressClient();
|
|
419
|
+
const o = opts || {};
|
|
420
|
+
const inputType = o.inputType || o.type || 'RTMP_INPUT';
|
|
421
|
+
const res = await i.createIngress(inputType, o);
|
|
422
|
+
this._emit('ingress-start', { ingressId: res && res.ingressId, inputType, room: o.roomName });
|
|
423
|
+
return res;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
async deleteIngress(ingressId)
|
|
427
|
+
{
|
|
428
|
+
const i = this._ingressClient();
|
|
429
|
+
const res = await i.deleteIngress(ingressId);
|
|
430
|
+
this._emit('ingress-stop', { ingressId });
|
|
431
|
+
return res;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// ----- SfuAdapter Phase-2 surface (LiveKit owns media, so most are
|
|
435
|
+
// client-side and surface as cooperative no-ops or REST mute hints) -----
|
|
436
|
+
|
|
437
|
+
async setConsumerPreferredLayers(consumerId, _layers)
|
|
438
|
+
{
|
|
439
|
+
if (!this._consumers.has(consumerId))
|
|
440
|
+
throw new WebRTCError('setConsumerPreferredLayers: unknown consumer', { code: 'WEBRTC_SFU_NO_CONSUMER' });
|
|
441
|
+
// LiveKit drives layer selection client-side (UpdateSubscription
|
|
442
|
+
// / SetSubscriptionPermissions); no server REST call is required.
|
|
443
|
+
this._emit('consumer-layers-change', { consumerId, layers: _layers || null });
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
async setConsumerPriority(consumerId, priority)
|
|
447
|
+
{
|
|
448
|
+
if (!this._consumers.has(consumerId))
|
|
449
|
+
throw new WebRTCError('setConsumerPriority: unknown consumer', { code: 'WEBRTC_SFU_NO_CONSUMER' });
|
|
450
|
+
this._emit('consumer-priority', { consumerId, priority });
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
async requestKeyFrame(consumerId)
|
|
454
|
+
{
|
|
455
|
+
if (!this._consumers.has(consumerId))
|
|
456
|
+
throw new WebRTCError('requestKeyFrame: unknown consumer', { code: 'WEBRTC_SFU_NO_CONSUMER' });
|
|
457
|
+
// LiveKit clients PLI through their own RTCP; no-op here.
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
async pauseConsumer(consumerId)
|
|
461
|
+
{
|
|
462
|
+
const c = this._consumers.get(consumerId);
|
|
463
|
+
if (!c)
|
|
464
|
+
throw new WebRTCError('pauseConsumer: unknown consumer', { code: 'WEBRTC_SFU_NO_CONSUMER' });
|
|
465
|
+
c.paused = true;
|
|
466
|
+
this._emit('consumer-pause', { consumerId });
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
async resumeConsumer(consumerId)
|
|
470
|
+
{
|
|
471
|
+
const c = this._consumers.get(consumerId);
|
|
472
|
+
if (!c)
|
|
473
|
+
throw new WebRTCError('resumeConsumer: unknown consumer', { code: 'WEBRTC_SFU_NO_CONSUMER' });
|
|
474
|
+
c.paused = false;
|
|
475
|
+
this._emit('consumer-resume', { consumerId });
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
async setTransportBitrates(transportId, bitrates)
|
|
479
|
+
{
|
|
480
|
+
if (!this._transports.has(transportId))
|
|
481
|
+
throw new WebRTCError('setTransportBitrates: unknown transport', { code: 'WEBRTC_SFU_NO_TRANSPORT' });
|
|
482
|
+
// LiveKit BWE is end-to-end inside its own client; we record the
|
|
483
|
+
// request so callers can read it back and emit so observers can
|
|
484
|
+
// count clamps.
|
|
485
|
+
const t = this._transports.get(transportId);
|
|
486
|
+
t.bitrates = bitrates || {};
|
|
487
|
+
this._emit('transport-bitrates', { transportId, bitrates: t.bitrates });
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
async produceData(transport, opts)
|
|
491
|
+
{
|
|
492
|
+
if (!transport || !this._transports.has(transport.id))
|
|
493
|
+
throw new WebRTCError('produceData: unknown transport', { code: 'WEBRTC_SFU_NO_TRANSPORT' });
|
|
494
|
+
const id = this._nextId('dataProducer');
|
|
495
|
+
const o = opts || {};
|
|
496
|
+
const dp = {
|
|
497
|
+
id, dataProducerId: id, transportId: transport.id,
|
|
498
|
+
label: o.label || '', protocol: o.protocol || '',
|
|
499
|
+
ordered: o.ordered !== false,
|
|
500
|
+
};
|
|
501
|
+
this._dataProducers = this._dataProducers || new Map();
|
|
502
|
+
this._dataProducers.set(id, dp);
|
|
503
|
+
this._emit('data-producer-new', { dataProducerId: id, transportId: transport.id, label: dp.label });
|
|
504
|
+
return dp;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
async consumeData(transport, dataProducerId, opts)
|
|
508
|
+
{
|
|
509
|
+
if (!transport || !this._transports.has(transport.id))
|
|
510
|
+
throw new WebRTCError('consumeData: unknown transport', { code: 'WEBRTC_SFU_NO_TRANSPORT' });
|
|
511
|
+
const id = this._nextId('dataConsumer');
|
|
512
|
+
const o = opts || {};
|
|
513
|
+
const dc = {
|
|
514
|
+
id, dataConsumerId: id, transportId: transport.id, dataProducerId,
|
|
515
|
+
label: '', protocol: '', ordered: o.ordered !== false,
|
|
516
|
+
};
|
|
517
|
+
this._dataConsumers = this._dataConsumers || new Map();
|
|
518
|
+
this._dataConsumers.set(id, dc);
|
|
519
|
+
this._emit('data-consumer-new', { dataConsumerId: id, transportId: transport.id, dataProducerId });
|
|
520
|
+
return dc;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
async observeAudioLevels(routerId, _opts)
|
|
524
|
+
{
|
|
525
|
+
if (!this._rooms.has(routerId))
|
|
526
|
+
throw new WebRTCError('observeAudioLevels: unknown router', { code: 'WEBRTC_SFU_NO_ROUTER' });
|
|
527
|
+
// LiveKit emits "active_speakers_changed" via the room webhook /
|
|
528
|
+
// server-events channel; expose a tiny handle that pushes those
|
|
529
|
+
// through our adapter `_emit` once the caller wires them.
|
|
530
|
+
const id = this._nextId('audioObserver');
|
|
531
|
+
const self = this;
|
|
532
|
+
const handle = {
|
|
533
|
+
id, kind: 'audio-level', routerId, closed: false,
|
|
534
|
+
close: async () => { handle.closed = true; self._emit('observer-close', { observerId: id, kind: 'audio-level' }); },
|
|
535
|
+
emit: (levels) => { if (!handle.closed) self._emit('audio-level', { observerId: id, routerId, levels }); },
|
|
536
|
+
};
|
|
537
|
+
this._emit('observer-new', { observerId: id, kind: 'audio-level', routerId });
|
|
538
|
+
return handle;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
async observeActiveSpeaker(routerId, _opts)
|
|
542
|
+
{
|
|
543
|
+
if (!this._rooms.has(routerId))
|
|
544
|
+
throw new WebRTCError('observeActiveSpeaker: unknown router', { code: 'WEBRTC_SFU_NO_ROUTER' });
|
|
545
|
+
const id = this._nextId('speakerObserver');
|
|
546
|
+
const self = this;
|
|
547
|
+
const handle = {
|
|
548
|
+
id, kind: 'active-speaker', routerId, closed: false,
|
|
549
|
+
close: async () => { handle.closed = true; self._emit('observer-close', { observerId: id, kind: 'active-speaker' }); },
|
|
550
|
+
emit: (producerId) => { if (!handle.closed) self._emit('active-speaker', { observerId: id, routerId, producerId }); },
|
|
551
|
+
};
|
|
552
|
+
this._emit('observer-new', { observerId: id, kind: 'active-speaker', routerId });
|
|
553
|
+
return handle;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
async pipeToRouter(_opts)
|
|
557
|
+
{
|
|
558
|
+
// LiveKit handles cross-node fanout itself across its cluster;
|
|
559
|
+
// application-level pipe is not part of the public REST surface.
|
|
560
|
+
throw new WebRTCError('pipeToRouter: not supported by LiveKit provider', { code: 'WEBRTC_SFU_NOT_SUPPORTED' });
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
async getProducerStats(producerId)
|
|
564
|
+
{
|
|
565
|
+
if (!this._producers.has(producerId))
|
|
566
|
+
throw new WebRTCError('getProducerStats: unknown producer', { code: 'WEBRTC_SFU_NO_PRODUCER' });
|
|
567
|
+
return [];
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
async getConsumerStats(consumerId)
|
|
571
|
+
{
|
|
572
|
+
if (!this._consumers.has(consumerId))
|
|
573
|
+
throw new WebRTCError('getConsumerStats: unknown consumer', { code: 'WEBRTC_SFU_NO_CONSUMER' });
|
|
574
|
+
return [];
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
async getTransportStats(transportId)
|
|
578
|
+
{
|
|
579
|
+
if (!this._transports.has(transportId))
|
|
580
|
+
throw new WebRTCError('getTransportStats: unknown transport', { code: 'WEBRTC_SFU_NO_TRANSPORT' });
|
|
581
|
+
return [];
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
async enableTraceEvent(routerId, types)
|
|
585
|
+
{
|
|
586
|
+
if (!this._rooms.has(routerId))
|
|
587
|
+
throw new WebRTCError('enableTraceEvent: unknown router', { code: 'WEBRTC_SFU_NO_ROUTER' });
|
|
588
|
+
this._emit('trace-enabled', { routerId, types: Array.isArray(types) ? types : [] });
|
|
589
|
+
}
|
|
286
590
|
}
|
|
287
591
|
|
|
288
592
|
/**
|