@zero-server/sdk 0.9.6 → 0.9.8
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 +54 -53
- package/index.js +116 -4
- package/lib/app.js +22 -22
- package/lib/auth/authorize.js +11 -11
- package/lib/auth/enrollment.js +5 -5
- package/lib/auth/jwt.js +9 -9
- package/lib/auth/oauth.js +1 -1
- package/lib/auth/session.js +5 -5
- package/lib/auth/trustedDevice.js +2 -2
- package/lib/auth/twoFactor.js +11 -11
- package/lib/auth/webauthn.js +6 -6
- package/lib/body/json.js +1 -1
- package/lib/body/raw.js +1 -1
- package/lib/body/rawBuffer.js +1 -1
- package/lib/body/text.js +1 -1
- package/lib/body/urlencoded.js +3 -3
- package/lib/cli.js +43 -28
- package/lib/cluster.js +3 -3
- package/lib/debug.js +10 -10
- package/lib/env/index.js +11 -11
- package/lib/errors.js +131 -16
- package/lib/fetch/index.js +1 -1
- package/lib/grpc/call.js +14 -14
- package/lib/grpc/client.js +4 -4
- package/lib/grpc/codec.js +7 -7
- package/lib/grpc/credentials.js +2 -2
- package/lib/grpc/frame.js +2 -2
- package/lib/grpc/health.js +3 -3
- package/lib/grpc/index.js +3 -3
- package/lib/grpc/metadata.js +3 -3
- package/lib/grpc/proto.js +5 -5
- package/lib/grpc/reflection.js +2 -2
- package/lib/grpc/server.js +3 -3
- package/lib/grpc/status.js +2 -2
- package/lib/grpc/watch.js +1 -1
- package/lib/http/request.js +13 -13
- package/lib/http/response.js +2 -2
- package/lib/lifecycle.js +5 -5
- package/lib/middleware/compress.js +4 -4
- package/lib/observe/health.js +1 -1
- package/lib/observe/index.js +1 -1
- package/lib/observe/logger.js +3 -3
- package/lib/observe/metrics.js +4 -4
- package/lib/observe/tracing.js +4 -4
- package/lib/orm/adapters/json.js +1 -1
- package/lib/orm/adapters/memory.js +2 -2
- package/lib/orm/adapters/mongo.js +2 -2
- package/lib/orm/adapters/mysql.js +2 -2
- package/lib/orm/adapters/postgres.js +2 -2
- package/lib/orm/adapters/sqlite.js +3 -3
- package/lib/orm/audit.js +1 -1
- package/lib/orm/index.js +7 -7
- package/lib/orm/migrate.js +1 -1
- package/lib/orm/model.js +15 -15
- package/lib/orm/procedures.js +1 -1
- package/lib/orm/profiler.js +1 -1
- package/lib/orm/query.js +9 -9
- package/lib/orm/schema.js +1 -1
- package/lib/orm/seed/data/person.js +1 -1
- package/lib/orm/seed/fake.js +10 -10
- package/lib/orm/seed/index.js +4 -4
- package/lib/orm/seed/rng.js +1 -1
- package/lib/orm/snapshot.js +3 -3
- package/lib/orm/tenancy.js +6 -6
- package/lib/orm/views.js +1 -1
- package/lib/router/index.js +9 -9
- package/lib/webrtc/bot.js +405 -0
- package/lib/webrtc/cli.js +182 -0
- package/lib/webrtc/cluster.js +338 -0
- package/lib/webrtc/e2ee.js +274 -0
- package/lib/webrtc/ice.js +363 -0
- package/lib/webrtc/index.js +212 -0
- package/lib/webrtc/joinToken.js +171 -0
- package/lib/webrtc/observe.js +260 -0
- package/lib/webrtc/peer.js +143 -0
- package/lib/webrtc/room.js +184 -0
- package/lib/webrtc/sdp.js +503 -0
- package/lib/webrtc/sfu/index.js +251 -0
- package/lib/webrtc/sfu/livekit.js +304 -0
- package/lib/webrtc/sfu/mediasoup.js +357 -0
- package/lib/webrtc/sfu/memory.js +221 -0
- package/lib/webrtc/signaling.js +590 -0
- package/lib/webrtc/stun.js +484 -0
- package/lib/webrtc/turn/codec.js +370 -0
- package/lib/webrtc/turn/credentials.js +156 -0
- package/lib/webrtc/turn/server.js +648 -0
- package/package.json +2 -2
- package/types/body.d.ts +82 -14
- package/types/cli.d.ts +40 -2
- package/types/index.d.ts +19 -6
- package/types/middleware.d.ts +18 -72
- package/types/orm.d.ts +4 -13
- package/types/request.d.ts +3 -3
- package/types/webrtc.d.ts +501 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module webrtc/sfu/mediasoup
|
|
3
|
+
* @description mediasoup-backed SFU adapter (peerDependency on
|
|
4
|
+
* `mediasoup`). Wraps a `Worker` plus one `Router` per `createRouter()`
|
|
5
|
+
* call and delegates produce/consume/pause/resume/close/stats to the
|
|
6
|
+
* native objects. Adapter events are uniform via `onEvent`.
|
|
7
|
+
*
|
|
8
|
+
* @example | Production setup with custom RTP port range and announced IP
|
|
9
|
+
* // npm install mediasoup
|
|
10
|
+
* const { MediasoupSfuAdapter } = require('@zero-server/webrtc');
|
|
11
|
+
*
|
|
12
|
+
* const sfu = new MediasoupSfuAdapter({
|
|
13
|
+
* workerSettings: {
|
|
14
|
+
* logLevel: 'warn',
|
|
15
|
+
* rtcMinPort: 40000,
|
|
16
|
+
* rtcMaxPort: 49999,
|
|
17
|
+
* },
|
|
18
|
+
* webRtcTransportOptions: {
|
|
19
|
+
* listenIps: [{ ip: '0.0.0.0', announcedIp: process.env.PUBLIC_IP }],
|
|
20
|
+
* enableUdp: true,
|
|
21
|
+
* enableTcp: true,
|
|
22
|
+
* preferUdp: true,
|
|
23
|
+
* initialAvailableOutgoingBitrate: 800_000,
|
|
24
|
+
* },
|
|
25
|
+
* });
|
|
26
|
+
*
|
|
27
|
+
* @example | One router per room, lazy on first join
|
|
28
|
+
* const routersByRoom = new Map();
|
|
29
|
+
*
|
|
30
|
+
* async function getRouter(roomName) {
|
|
31
|
+
* let r = routersByRoom.get(roomName);
|
|
32
|
+
* if (!r) {
|
|
33
|
+
* r = await sfu.createRouter({ room: roomName });
|
|
34
|
+
* routersByRoom.set(roomName, r);
|
|
35
|
+
* }
|
|
36
|
+
* return r;
|
|
37
|
+
* }
|
|
38
|
+
*
|
|
39
|
+
* hub.on('join', async ({ peer, room }) => {
|
|
40
|
+
* const router = await getRouter(room.name);
|
|
41
|
+
* const transport = await sfu.createTransport(router, peer);
|
|
42
|
+
* peer.send('sfu-ready', { transportId: transport.id });
|
|
43
|
+
* });
|
|
44
|
+
*
|
|
45
|
+
* @example | Inject a stub for unit tests
|
|
46
|
+
* const stubMediasoup = {
|
|
47
|
+
* createWorker: async () => ({
|
|
48
|
+
* createRouter: async () => fakeRouter,
|
|
49
|
+
* close: () => {},
|
|
50
|
+
* }),
|
|
51
|
+
* };
|
|
52
|
+
* const sfu = new MediasoupSfuAdapter({ mediasoup: stubMediasoup });
|
|
53
|
+
*/
|
|
54
|
+
'use strict';
|
|
55
|
+
|
|
56
|
+
const { SfuAdapter } = require('./index');
|
|
57
|
+
const { WebRTCError } = require('../../errors');
|
|
58
|
+
|
|
59
|
+
const DEFAULT_MEDIA_CODECS = [
|
|
60
|
+
{ kind: 'audio', mimeType: 'audio/opus', clockRate: 48000, channels: 2 },
|
|
61
|
+
{ kind: 'video', mimeType: 'video/VP8', clockRate: 90000 },
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
const DEFAULT_WEBRTC_TRANSPORT_OPTS = {
|
|
65
|
+
listenIps: [{ ip: '0.0.0.0', announcedIp: null }],
|
|
66
|
+
enableUdp: true,
|
|
67
|
+
enableTcp: true,
|
|
68
|
+
preferUdp: true,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
class MediasoupSfuAdapter extends SfuAdapter
|
|
72
|
+
{
|
|
73
|
+
/**
|
|
74
|
+
* @param {object} [opts]
|
|
75
|
+
* @param {object} [opts.mediasoup] Injected mediasoup module (testing); defaults to `require('mediasoup')`.
|
|
76
|
+
* @param {object} [opts.worker] Pre-created `mediasoup.Worker`; bypasses the lazy worker bootstrap.
|
|
77
|
+
* @param {object} [opts.workerSettings] Forwarded to `mediasoup.createWorker(...)`.
|
|
78
|
+
* @param {Array} [opts.mediaCodecs] Default router media codecs.
|
|
79
|
+
* @param {object} [opts.webRtcTransportOptions] Default `router.createWebRtcTransport(...)` options.
|
|
80
|
+
*/
|
|
81
|
+
constructor(opts)
|
|
82
|
+
{
|
|
83
|
+
super();
|
|
84
|
+
const o = opts || {};
|
|
85
|
+
|
|
86
|
+
this._mediasoup = o.mediasoup || _tryRequireMediasoup();
|
|
87
|
+
this._workerSettings = o.workerSettings || {};
|
|
88
|
+
this._mediaCodecs = o.mediaCodecs || DEFAULT_MEDIA_CODECS;
|
|
89
|
+
this._webRtcTransportOpts = o.webRtcTransportOptions || DEFAULT_WEBRTC_TRANSPORT_OPTS;
|
|
90
|
+
this._worker = o.worker || null;
|
|
91
|
+
this._workerPromise = null;
|
|
92
|
+
|
|
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
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Lazily create (or return) the single shared mediasoup Worker.
|
|
102
|
+
* Returns the native Worker handle.
|
|
103
|
+
*/
|
|
104
|
+
async _ensureWorker()
|
|
105
|
+
{
|
|
106
|
+
if (this._worker) return this._worker;
|
|
107
|
+
if (!this._workerPromise)
|
|
108
|
+
{
|
|
109
|
+
this._workerPromise = Promise.resolve(this._mediasoup.createWorker(this._workerSettings))
|
|
110
|
+
.then((w) =>
|
|
111
|
+
{
|
|
112
|
+
this._worker = w;
|
|
113
|
+
if (typeof w.on === 'function')
|
|
114
|
+
{
|
|
115
|
+
w.on('died', (err) => this._emit('worker-died', { error: err && err.message }));
|
|
116
|
+
}
|
|
117
|
+
return w;
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
return this._workerPromise;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async createRouter(opts)
|
|
124
|
+
{
|
|
125
|
+
const worker = await this._ensureWorker();
|
|
126
|
+
const mediaCodecs = (opts && opts.mediaCodecs) || this._mediaCodecs;
|
|
127
|
+
const router = await worker.createRouter({ mediaCodecs });
|
|
128
|
+
this._routers.set(router.id, router);
|
|
129
|
+
if (typeof router.observer === 'object' && router.observer && typeof router.observer.on === 'function')
|
|
130
|
+
{
|
|
131
|
+
router.observer.on('close', () =>
|
|
132
|
+
{
|
|
133
|
+
this._routers.delete(router.id);
|
|
134
|
+
this._emit('router-close', { routerId: router.id });
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
this._emit('router-new', { routerId: router.id });
|
|
138
|
+
return {
|
|
139
|
+
id: router.id,
|
|
140
|
+
routerId: router.id,
|
|
141
|
+
rtpCapabilities: router.rtpCapabilities,
|
|
142
|
+
_native: router,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async createTransport(router, peer)
|
|
147
|
+
{
|
|
148
|
+
const routerId = router && router.id;
|
|
149
|
+
const native = routerId && this._routers.get(routerId);
|
|
150
|
+
if (!native)
|
|
151
|
+
{
|
|
152
|
+
throw new WebRTCError('createTransport: unknown router', { code: 'WEBRTC_SFU_NO_ROUTER' });
|
|
153
|
+
}
|
|
154
|
+
const transport = await native.createWebRtcTransport({
|
|
155
|
+
...this._webRtcTransportOpts,
|
|
156
|
+
appData: { peer: peer || null },
|
|
157
|
+
});
|
|
158
|
+
this._transports.set(transport.id, transport);
|
|
159
|
+
this._routerOf.set(transport.id, routerId);
|
|
160
|
+
if (typeof transport.observer === 'object' && transport.observer && typeof transport.observer.on === 'function')
|
|
161
|
+
{
|
|
162
|
+
transport.observer.on('close', () =>
|
|
163
|
+
{
|
|
164
|
+
this._transports.delete(transport.id);
|
|
165
|
+
this._routerOf.delete(transport.id);
|
|
166
|
+
this._emit('transport-close', { transportId: transport.id });
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
this._emit('transport-new', { transportId: transport.id, routerId, peerId: peer && peer.id });
|
|
170
|
+
return {
|
|
171
|
+
id: transport.id,
|
|
172
|
+
transportId: transport.id,
|
|
173
|
+
routerId,
|
|
174
|
+
peer: peer || null,
|
|
175
|
+
iceParameters: transport.iceParameters,
|
|
176
|
+
iceCandidates: transport.iceCandidates,
|
|
177
|
+
dtlsParameters: transport.dtlsParameters,
|
|
178
|
+
sctpParameters: transport.sctpParameters || null,
|
|
179
|
+
_native: transport,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async produce(transport, kind, rtpParameters)
|
|
184
|
+
{
|
|
185
|
+
if (kind !== 'audio' && kind !== 'video')
|
|
186
|
+
{
|
|
187
|
+
throw new WebRTCError('produce: kind must be "audio" or "video"', { code: 'WEBRTC_SFU_INVALID_KIND' });
|
|
188
|
+
}
|
|
189
|
+
const native = transport && this._transports.get(transport.id);
|
|
190
|
+
if (!native)
|
|
191
|
+
{
|
|
192
|
+
throw new WebRTCError('produce: unknown transport', { code: 'WEBRTC_SFU_NO_TRANSPORT' });
|
|
193
|
+
}
|
|
194
|
+
const producer = await native.produce({ kind, rtpParameters });
|
|
195
|
+
this._producers.set(producer.id, producer);
|
|
196
|
+
if (typeof producer.on === 'function')
|
|
197
|
+
{
|
|
198
|
+
producer.on('transportclose', () =>
|
|
199
|
+
{
|
|
200
|
+
this._producers.delete(producer.id);
|
|
201
|
+
this._emit('producer-close', { producerId: producer.id, reason: 'transport-close' });
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
this._emit('producer-new', { producerId: producer.id, transportId: transport.id, kind });
|
|
205
|
+
return {
|
|
206
|
+
id: producer.id,
|
|
207
|
+
producerId: producer.id,
|
|
208
|
+
transportId: transport.id,
|
|
209
|
+
kind,
|
|
210
|
+
rtpParameters,
|
|
211
|
+
paused: !!producer.paused,
|
|
212
|
+
_native: producer,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async consume(transport, producerId, rtpCapabilities)
|
|
217
|
+
{
|
|
218
|
+
const native = transport && this._transports.get(transport.id);
|
|
219
|
+
if (!native)
|
|
220
|
+
{
|
|
221
|
+
throw new WebRTCError('consume: unknown transport', { code: 'WEBRTC_SFU_NO_TRANSPORT' });
|
|
222
|
+
}
|
|
223
|
+
const routerId = this._routerOf.get(transport.id);
|
|
224
|
+
const router = routerId && this._routers.get(routerId);
|
|
225
|
+
if (router && typeof router.canConsume === 'function'
|
|
226
|
+
&& !router.canConsume({ producerId, rtpCapabilities }))
|
|
227
|
+
{
|
|
228
|
+
throw new WebRTCError('consume: router cannot consume producer with given rtpCapabilities',
|
|
229
|
+
{ code: 'WEBRTC_SFU_CANNOT_CONSUME' });
|
|
230
|
+
}
|
|
231
|
+
let consumer;
|
|
232
|
+
try
|
|
233
|
+
{
|
|
234
|
+
consumer = await native.consume({ producerId, rtpCapabilities });
|
|
235
|
+
}
|
|
236
|
+
catch (err)
|
|
237
|
+
{
|
|
238
|
+
throw new WebRTCError(`consume failed: ${err.message}`, { code: 'WEBRTC_SFU_CONSUME_FAILED', cause: err });
|
|
239
|
+
}
|
|
240
|
+
this._consumers.set(consumer.id, consumer);
|
|
241
|
+
if (typeof consumer.on === 'function')
|
|
242
|
+
{
|
|
243
|
+
consumer.on('transportclose', () =>
|
|
244
|
+
{
|
|
245
|
+
this._consumers.delete(consumer.id);
|
|
246
|
+
this._emit('consumer-close', { consumerId: consumer.id, reason: 'transport-close' });
|
|
247
|
+
});
|
|
248
|
+
consumer.on('producerclose', () =>
|
|
249
|
+
{
|
|
250
|
+
this._consumers.delete(consumer.id);
|
|
251
|
+
this._emit('consumer-close', { consumerId: consumer.id, reason: 'producer-close' });
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
this._emit('consumer-new', { consumerId: consumer.id, transportId: transport.id, producerId });
|
|
255
|
+
return {
|
|
256
|
+
id: consumer.id,
|
|
257
|
+
consumerId: consumer.id,
|
|
258
|
+
transportId: transport.id,
|
|
259
|
+
producerId,
|
|
260
|
+
kind: consumer.kind,
|
|
261
|
+
rtpParameters: consumer.rtpParameters,
|
|
262
|
+
rtpCapabilities,
|
|
263
|
+
_native: consumer,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async pauseProducer(producerId)
|
|
268
|
+
{
|
|
269
|
+
const p = this._producers.get(producerId);
|
|
270
|
+
if (!p)
|
|
271
|
+
{
|
|
272
|
+
throw new WebRTCError('pauseProducer: unknown producer', { code: 'WEBRTC_SFU_NO_PRODUCER' });
|
|
273
|
+
}
|
|
274
|
+
await p.pause();
|
|
275
|
+
this._emit('producer-pause', { producerId });
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async resumeProducer(producerId)
|
|
279
|
+
{
|
|
280
|
+
const p = this._producers.get(producerId);
|
|
281
|
+
if (!p)
|
|
282
|
+
{
|
|
283
|
+
throw new WebRTCError('resumeProducer: unknown producer', { code: 'WEBRTC_SFU_NO_PRODUCER' });
|
|
284
|
+
}
|
|
285
|
+
await p.resume();
|
|
286
|
+
this._emit('producer-resume', { producerId });
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async closeRouter(routerId)
|
|
290
|
+
{
|
|
291
|
+
const r = this._routers.get(routerId);
|
|
292
|
+
if (!r) return;
|
|
293
|
+
// Native router.close() cascades to its transports; the observer
|
|
294
|
+
// 'close' handlers we registered in createTransport/createRouter
|
|
295
|
+
// emit transport-close / router-close events for us. Avoid
|
|
296
|
+
// emitting here to prevent duplicates.
|
|
297
|
+
await r.close();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async stats(scope)
|
|
301
|
+
{
|
|
302
|
+
if (scope && this._routers.has(scope))
|
|
303
|
+
{
|
|
304
|
+
const r = this._routers.get(scope);
|
|
305
|
+
const native = typeof r.getStats === 'function' ? await r.getStats() : null;
|
|
306
|
+
return { kind: 'router', routerId: scope, native };
|
|
307
|
+
}
|
|
308
|
+
if (scope && this._transports.has(scope))
|
|
309
|
+
{
|
|
310
|
+
const t = this._transports.get(scope);
|
|
311
|
+
const native = typeof t.getStats === 'function' ? await t.getStats() : null;
|
|
312
|
+
return { kind: 'transport', transportId: scope, routerId: this._routerOf.get(scope), native };
|
|
313
|
+
}
|
|
314
|
+
return {
|
|
315
|
+
kind: 'global',
|
|
316
|
+
routers: this._routers.size,
|
|
317
|
+
transports: this._transports.size,
|
|
318
|
+
producers: this._producers.size,
|
|
319
|
+
consumers: this._consumers.size,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Best-effort shutdown: closes every router, then the worker if we own it.
|
|
325
|
+
*/
|
|
326
|
+
async close()
|
|
327
|
+
{
|
|
328
|
+
for (const id of [...this._routers.keys()])
|
|
329
|
+
{
|
|
330
|
+
try { await this.closeRouter(id); } catch (_) { /* swallow */ }
|
|
331
|
+
}
|
|
332
|
+
if (this._worker && typeof this._worker.close === 'function')
|
|
333
|
+
{
|
|
334
|
+
try { await this._worker.close(); } catch (_) { /* swallow */ }
|
|
335
|
+
}
|
|
336
|
+
this._worker = null;
|
|
337
|
+
this._workerPromise = null;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* @private
|
|
343
|
+
* Try to `require('mediasoup')`; throw a clean install hint when missing.
|
|
344
|
+
*/
|
|
345
|
+
function _tryRequireMediasoup()
|
|
346
|
+
{
|
|
347
|
+
try { return require('mediasoup'); }
|
|
348
|
+
catch (err)
|
|
349
|
+
{
|
|
350
|
+
throw new WebRTCError(
|
|
351
|
+
"SFU adapter 'mediasoup' requires the 'mediasoup' peerDependency: npm install mediasoup",
|
|
352
|
+
{ code: 'WEBRTC_SFU_NOT_INSTALLED', cause: err },
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
module.exports = { MediasoupSfuAdapter };
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module webrtc/sfu/memory
|
|
3
|
+
* @description In-process "memory" SFU adapter. Bookkeeping-only
|
|
4
|
+
* passthrough that records producers/consumers and emits adapter events
|
|
5
|
+
* without forwarding media packets. Ideal for unit tests, CI, and local
|
|
6
|
+
* dev — use a native adapter for real media plane work.
|
|
7
|
+
*
|
|
8
|
+
* @example | Use the memory adapter inside a vitest suite
|
|
9
|
+
* const { MemorySfuAdapter } = require('@zero-server/webrtc');
|
|
10
|
+
* const sfu = new MemorySfuAdapter();
|
|
11
|
+
*
|
|
12
|
+
* const events = [];
|
|
13
|
+
* sfu.onEvent((event, payload) => events.push({ event, payload }));
|
|
14
|
+
*
|
|
15
|
+
* const router = await sfu.createRouter({ room: 'lobby' });
|
|
16
|
+
* const transport = await sfu.createTransport(router, { id: 'peer-1' });
|
|
17
|
+
* const producer = await sfu.produce(transport, 'audio', { codec: 'opus' });
|
|
18
|
+
* const consumer = await sfu.consume(transport, producer.id, {});
|
|
19
|
+
*
|
|
20
|
+
* expect(events).toEqual([
|
|
21
|
+
* { event: 'router-new', payload: { routerId: router.id } },
|
|
22
|
+
* { event: 'transport-new', payload: expect.any(Object) },
|
|
23
|
+
* { event: 'producer-new', payload: expect.objectContaining({ kind: 'audio' }) },
|
|
24
|
+
* { event: 'consumer-new', payload: expect.objectContaining({ producerId: producer.id }) },
|
|
25
|
+
* ]);
|
|
26
|
+
*
|
|
27
|
+
* @example | Drive it through `loadSfuAdapter`
|
|
28
|
+
* const sfu = loadSfuAdapter('memory');
|
|
29
|
+
* const stats = await sfu.stats();
|
|
30
|
+
* console.log(stats); // { routers, transports, producers, consumers }
|
|
31
|
+
*/
|
|
32
|
+
'use strict';
|
|
33
|
+
|
|
34
|
+
const { SfuAdapter } = require('./index');
|
|
35
|
+
const { WebRTCError } = require('../../errors');
|
|
36
|
+
|
|
37
|
+
class MemorySfuAdapter extends SfuAdapter
|
|
38
|
+
{
|
|
39
|
+
constructor(opts)
|
|
40
|
+
{
|
|
41
|
+
super();
|
|
42
|
+
this._opts = opts || {};
|
|
43
|
+
this._counter = 0;
|
|
44
|
+
this._routers = new Map(); // routerId -> { id, opts, transports:Set, closed }
|
|
45
|
+
this._transports = new Map(); // transportId -> { id, routerId, peer, producers:Set, consumers:Set, closed }
|
|
46
|
+
this._producers = new Map(); // producerId -> { id, transportId, kind, rtpParams, paused, closed }
|
|
47
|
+
this._consumers = new Map(); // consumerId -> { id, transportId, producerId, rtpCaps, closed }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
_nextId(prefix)
|
|
51
|
+
{
|
|
52
|
+
this._counter += 1;
|
|
53
|
+
return `${prefix}-${this._counter}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async createRouter(opts)
|
|
57
|
+
{
|
|
58
|
+
const id = this._nextId('router');
|
|
59
|
+
const router = { id, opts: opts || {}, transports: new Set(), closed: false };
|
|
60
|
+
this._routers.set(id, router);
|
|
61
|
+
this._emit('router-new', { routerId: id });
|
|
62
|
+
return { id, routerId: id };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async createTransport(router, peer)
|
|
66
|
+
{
|
|
67
|
+
const routerId = router && router.id;
|
|
68
|
+
const r = routerId && this._routers.get(routerId);
|
|
69
|
+
if (!r || r.closed)
|
|
70
|
+
{
|
|
71
|
+
throw new WebRTCError('createTransport: unknown router', { code: 'WEBRTC_SFU_NO_ROUTER' });
|
|
72
|
+
}
|
|
73
|
+
const id = this._nextId('transport');
|
|
74
|
+
const t = {
|
|
75
|
+
id,
|
|
76
|
+
transportId: id,
|
|
77
|
+
routerId,
|
|
78
|
+
peer: peer || null,
|
|
79
|
+
producers: new Set(),
|
|
80
|
+
consumers: new Set(),
|
|
81
|
+
closed: false,
|
|
82
|
+
iceParameters: { usernameFragment: id, password: id },
|
|
83
|
+
dtlsParameters: { role: 'auto', fingerprints: [] },
|
|
84
|
+
};
|
|
85
|
+
this._transports.set(id, t);
|
|
86
|
+
r.transports.add(id);
|
|
87
|
+
this._emit('transport-new', { transportId: id, routerId, peerId: peer && peer.id });
|
|
88
|
+
return t;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async produce(transport, kind, rtpParams)
|
|
92
|
+
{
|
|
93
|
+
if (kind !== 'audio' && kind !== 'video')
|
|
94
|
+
{
|
|
95
|
+
throw new WebRTCError('produce: kind must be "audio" or "video"', { code: 'WEBRTC_SFU_INVALID_KIND' });
|
|
96
|
+
}
|
|
97
|
+
const t = transport && this._transports.get(transport.id);
|
|
98
|
+
if (!t || t.closed)
|
|
99
|
+
{
|
|
100
|
+
throw new WebRTCError('produce: unknown transport', { code: 'WEBRTC_SFU_NO_TRANSPORT' });
|
|
101
|
+
}
|
|
102
|
+
const id = this._nextId('producer');
|
|
103
|
+
const p = { id, producerId: id, transportId: t.id, kind, rtpParams: rtpParams || {}, paused: false, closed: false };
|
|
104
|
+
this._producers.set(id, p);
|
|
105
|
+
t.producers.add(id);
|
|
106
|
+
this._emit('producer-new', { producerId: id, transportId: t.id, kind });
|
|
107
|
+
return p;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async consume(transport, producerId, rtpCaps)
|
|
111
|
+
{
|
|
112
|
+
const t = transport && this._transports.get(transport.id);
|
|
113
|
+
if (!t || t.closed)
|
|
114
|
+
{
|
|
115
|
+
throw new WebRTCError('consume: unknown transport', { code: 'WEBRTC_SFU_NO_TRANSPORT' });
|
|
116
|
+
}
|
|
117
|
+
const prod = this._producers.get(producerId);
|
|
118
|
+
if (!prod || prod.closed)
|
|
119
|
+
{
|
|
120
|
+
throw new WebRTCError('consume: unknown producer', { code: 'WEBRTC_SFU_NO_PRODUCER' });
|
|
121
|
+
}
|
|
122
|
+
const id = this._nextId('consumer');
|
|
123
|
+
const c = {
|
|
124
|
+
id,
|
|
125
|
+
consumerId: id,
|
|
126
|
+
transportId: t.id,
|
|
127
|
+
producerId,
|
|
128
|
+
kind: prod.kind,
|
|
129
|
+
rtpParams: prod.rtpParams,
|
|
130
|
+
rtpCaps: rtpCaps || {},
|
|
131
|
+
closed: false,
|
|
132
|
+
};
|
|
133
|
+
this._consumers.set(id, c);
|
|
134
|
+
t.consumers.add(id);
|
|
135
|
+
this._emit('consumer-new', { consumerId: id, transportId: t.id, producerId });
|
|
136
|
+
return c;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async pauseProducer(producerId)
|
|
140
|
+
{
|
|
141
|
+
const p = this._producers.get(producerId);
|
|
142
|
+
if (!p || p.closed)
|
|
143
|
+
{
|
|
144
|
+
throw new WebRTCError('pauseProducer: unknown producer', { code: 'WEBRTC_SFU_NO_PRODUCER' });
|
|
145
|
+
}
|
|
146
|
+
if (!p.paused)
|
|
147
|
+
{
|
|
148
|
+
p.paused = true;
|
|
149
|
+
this._emit('producer-pause', { producerId });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async resumeProducer(producerId)
|
|
154
|
+
{
|
|
155
|
+
const p = this._producers.get(producerId);
|
|
156
|
+
if (!p || p.closed)
|
|
157
|
+
{
|
|
158
|
+
throw new WebRTCError('resumeProducer: unknown producer', { code: 'WEBRTC_SFU_NO_PRODUCER' });
|
|
159
|
+
}
|
|
160
|
+
if (p.paused)
|
|
161
|
+
{
|
|
162
|
+
p.paused = false;
|
|
163
|
+
this._emit('producer-resume', { producerId });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async closeRouter(routerId)
|
|
168
|
+
{
|
|
169
|
+
const r = this._routers.get(routerId);
|
|
170
|
+
if (!r) return;
|
|
171
|
+
for (const tid of r.transports)
|
|
172
|
+
{
|
|
173
|
+
const t = this._transports.get(tid);
|
|
174
|
+
if (!t) continue;
|
|
175
|
+
for (const pid of t.producers)
|
|
176
|
+
{
|
|
177
|
+
const p = this._producers.get(pid);
|
|
178
|
+
if (p) { p.closed = true; this._emit('producer-close', { producerId: pid }); }
|
|
179
|
+
this._producers.delete(pid);
|
|
180
|
+
}
|
|
181
|
+
for (const cid of t.consumers)
|
|
182
|
+
{
|
|
183
|
+
const c = this._consumers.get(cid);
|
|
184
|
+
if (c) { c.closed = true; this._emit('consumer-close', { consumerId: cid }); }
|
|
185
|
+
this._consumers.delete(cid);
|
|
186
|
+
}
|
|
187
|
+
t.closed = true;
|
|
188
|
+
this._emit('transport-close', { transportId: tid });
|
|
189
|
+
this._transports.delete(tid);
|
|
190
|
+
}
|
|
191
|
+
r.closed = true;
|
|
192
|
+
this._emit('router-close', { routerId });
|
|
193
|
+
this._routers.delete(routerId);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async stats(scope)
|
|
197
|
+
{
|
|
198
|
+
if (scope && this._routers.has(scope))
|
|
199
|
+
{
|
|
200
|
+
const r = this._routers.get(scope);
|
|
201
|
+
return { kind: 'router', routerId: scope, transports: r.transports.size };
|
|
202
|
+
}
|
|
203
|
+
if (scope && this._transports.has(scope))
|
|
204
|
+
{
|
|
205
|
+
const t = this._transports.get(scope);
|
|
206
|
+
return {
|
|
207
|
+
kind: 'transport', transportId: scope, routerId: t.routerId,
|
|
208
|
+
producers: t.producers.size, consumers: t.consumers.size,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
kind: 'global',
|
|
213
|
+
routers: this._routers.size,
|
|
214
|
+
transports: this._transports.size,
|
|
215
|
+
producers: this._producers.size,
|
|
216
|
+
consumers: this._consumers.size,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
module.exports = { MemorySfuAdapter };
|