@zero-server/webrtc 0.9.7
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/LICENSE +21 -0
- package/README.md +37 -0
- package/index.d.ts +2 -0
- package/index.js +53 -0
- package/lib/auth/index.js +1 -0
- package/lib/debug.js +372 -0
- package/lib/errors.js +1 -0
- package/lib/middleware/index.js +1 -0
- package/lib/observe/index.js +1 -0
- package/lib/webrtc/bot.js +361 -0
- package/lib/webrtc/cli.js +182 -0
- package/lib/webrtc/cluster.js +350 -0
- package/lib/webrtc/e2ee.js +282 -0
- package/lib/webrtc/ice.js +370 -0
- package/lib/webrtc/index.js +132 -0
- package/lib/webrtc/joinToken.js +116 -0
- package/lib/webrtc/observe.js +229 -0
- package/lib/webrtc/peer.js +116 -0
- package/lib/webrtc/room.js +171 -0
- package/lib/webrtc/sdp.js +508 -0
- package/lib/webrtc/sfu/index.js +201 -0
- package/lib/webrtc/sfu/livekit.js +301 -0
- package/lib/webrtc/sfu/mediasoup.js +317 -0
- package/lib/webrtc/sfu/memory.js +204 -0
- package/lib/webrtc/signaling.js +546 -0
- package/lib/webrtc/stun.js +492 -0
- package/lib/webrtc/turn/codec.js +370 -0
- package/lib/webrtc/turn/credentials.js +141 -0
- package/lib/webrtc/turn/server.js +633 -0
- package/lib/ws/index.js +1 -0
- package/package.json +62 -0
- package/types/app.d.ts +223 -0
- package/types/auth.d.ts +520 -0
- package/types/body.d.ts +14 -0
- package/types/cli.d.ts +2 -0
- package/types/cluster.d.ts +75 -0
- package/types/env.d.ts +80 -0
- package/types/errors.d.ts +316 -0
- package/types/fetch.d.ts +43 -0
- package/types/grpc.d.ts +432 -0
- package/types/index.d.ts +396 -0
- package/types/lifecycle.d.ts +60 -0
- package/types/middleware.d.ts +320 -0
- package/types/observe.d.ts +304 -0
- package/types/orm.d.ts +1887 -0
- package/types/request.d.ts +109 -0
- package/types/response.d.ts +157 -0
- package/types/router.d.ts +78 -0
- package/types/sse.d.ts +78 -0
- package/types/webrtc.d.ts +501 -0
- package/types/websocket.d.ts +126 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module webrtc/sfu/livekit
|
|
3
|
+
* @description LiveKit-backed SFU adapter (peerDependency on `livekit-server-sdk`).
|
|
4
|
+
*
|
|
5
|
+
* LiveKit's media plane is controlled remotely: rooms live on the
|
|
6
|
+
* LiveKit server, participants connect directly with a signed JWT, and
|
|
7
|
+
* the server SDK exposes a control-plane REST API. This adapter maps
|
|
8
|
+
* the {@link SfuAdapter} contract onto that model:
|
|
9
|
+
*
|
|
10
|
+
* - createRouter(opts) -> RoomServiceClient.createRoom(...)
|
|
11
|
+
* - createTransport(router,peer) -> mints an AccessToken (the "transport"
|
|
12
|
+
* handle is the URL + JWT the peer
|
|
13
|
+
* uses to connect to LiveKit directly)
|
|
14
|
+
* - produce / consume -> local bookkeeping; LiveKit handles
|
|
15
|
+
* the actual media plane client-side
|
|
16
|
+
* - pauseProducer / resume -> RoomServiceClient.mutePublishedTrack()
|
|
17
|
+
* when the producer was registered
|
|
18
|
+
* with a `{room, identity, trackSid}`
|
|
19
|
+
* hint; otherwise emits the event
|
|
20
|
+
* without touching the server
|
|
21
|
+
* - closeRouter(routerId) -> RoomServiceClient.deleteRoom(...)
|
|
22
|
+
* - stats() -> RoomServiceClient.listRooms() /
|
|
23
|
+
* listParticipants(...) plus local
|
|
24
|
+
* counters
|
|
25
|
+
*
|
|
26
|
+
* `livekit-server-sdk` is loaded lazily. Tests inject a stub via
|
|
27
|
+
* `opts.livekit`; in production the constructor `require`s the package
|
|
28
|
+
* and throws `WEBRTC_SFU_NOT_INSTALLED` if it is missing.
|
|
29
|
+
*/
|
|
30
|
+
'use strict';
|
|
31
|
+
|
|
32
|
+
const { SfuAdapter } = require('./index');
|
|
33
|
+
const { WebRTCError } = require('../../errors');
|
|
34
|
+
|
|
35
|
+
const DEFAULT_TOKEN_TTL = '1h';
|
|
36
|
+
|
|
37
|
+
class LiveKitSfuAdapter extends SfuAdapter
|
|
38
|
+
{
|
|
39
|
+
/**
|
|
40
|
+
* @param {object} opts
|
|
41
|
+
* @param {string} opts.url LiveKit server URL (wss://...).
|
|
42
|
+
* @param {string} opts.apiKey LiveKit API key.
|
|
43
|
+
* @param {string} opts.apiSecret LiveKit API secret.
|
|
44
|
+
* @param {object} [opts.livekit] Injected `livekit-server-sdk` module (testing).
|
|
45
|
+
* @param {object} [opts.client] Pre-built `RoomServiceClient` (testing).
|
|
46
|
+
* @param {object} [opts.defaultRoomOpts] Forwarded to `createRoom()` when fields are missing.
|
|
47
|
+
* @param {object} [opts.defaultGrants] Default `{canPublish, canSubscribe, ...}` for minted tokens.
|
|
48
|
+
* @param {string} [opts.tokenTtl='1h'] AccessToken TTL.
|
|
49
|
+
*/
|
|
50
|
+
constructor(opts)
|
|
51
|
+
{
|
|
52
|
+
super();
|
|
53
|
+
const o = opts || {};
|
|
54
|
+
if (!o.url || !o.apiKey || !o.apiSecret)
|
|
55
|
+
{
|
|
56
|
+
throw new WebRTCError(
|
|
57
|
+
'LiveKitSfuAdapter requires { url, apiKey, apiSecret }',
|
|
58
|
+
{ code: 'WEBRTC_SFU_INVALID_CONFIG' },
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
this._livekit = o.livekit || _tryRequireLivekit();
|
|
62
|
+
this._url = o.url;
|
|
63
|
+
this._apiKey = o.apiKey;
|
|
64
|
+
this._apiSecret = o.apiSecret;
|
|
65
|
+
this._defaultRoomOpts = o.defaultRoomOpts || {};
|
|
66
|
+
this._defaultGrants = o.defaultGrants || { canPublish: true, canSubscribe: true };
|
|
67
|
+
this._tokenTtl = o.tokenTtl || DEFAULT_TOKEN_TTL;
|
|
68
|
+
|
|
69
|
+
this._client = o.client || new this._livekit.RoomServiceClient(this._url, this._apiKey, this._apiSecret);
|
|
70
|
+
|
|
71
|
+
this._rooms = new Map(); // routerId -> { name, opts }
|
|
72
|
+
this._transports = new Map(); // transportId -> { identity, room, token }
|
|
73
|
+
this._producers = new Map(); // producerId -> { kind, transportId, room, identity, trackSid? }
|
|
74
|
+
this._consumers = new Map(); // consumerId -> { producerId, transportId }
|
|
75
|
+
|
|
76
|
+
this._idSeq = 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
_nextId(prefix)
|
|
80
|
+
{
|
|
81
|
+
this._idSeq += 1;
|
|
82
|
+
return `${prefix}-${this._idSeq}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Create a LiveKit room. `opts.name` overrides the auto-generated name.
|
|
87
|
+
* Returns a router handle whose `id` is the room name.
|
|
88
|
+
*/
|
|
89
|
+
async createRouter(opts)
|
|
90
|
+
{
|
|
91
|
+
const o = { ...this._defaultRoomOpts, ...(opts || {}) };
|
|
92
|
+
const name = o.name || this._nextId('room');
|
|
93
|
+
const room = await this._client.createRoom({ ...o, name });
|
|
94
|
+
const id = room && room.name ? room.name : name;
|
|
95
|
+
this._rooms.set(id, { name: id, opts: o, native: room });
|
|
96
|
+
this._emit('router-new', { routerId: id });
|
|
97
|
+
return { id, routerId: id, name: id, _native: room };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Mint an AccessToken for `peer` to join the LiveKit room. Returns
|
|
102
|
+
* a transport handle containing the JWT and URL the peer hands to
|
|
103
|
+
* the LiveKit client SDK.
|
|
104
|
+
*/
|
|
105
|
+
async createTransport(router, peer)
|
|
106
|
+
{
|
|
107
|
+
const routerId = router && router.id;
|
|
108
|
+
const room = routerId && this._rooms.get(routerId);
|
|
109
|
+
if (!room)
|
|
110
|
+
{
|
|
111
|
+
throw new WebRTCError('createTransport: unknown router', { code: 'WEBRTC_SFU_NO_ROUTER' });
|
|
112
|
+
}
|
|
113
|
+
const identity = (peer && peer.id) || this._nextId('peer');
|
|
114
|
+
const at = new this._livekit.AccessToken(this._apiKey, this._apiSecret, {
|
|
115
|
+
identity,
|
|
116
|
+
ttl: this._tokenTtl,
|
|
117
|
+
name: (peer && peer.name) || identity,
|
|
118
|
+
});
|
|
119
|
+
at.addGrant({ roomJoin: true, room: routerId, ...this._defaultGrants });
|
|
120
|
+
const token = await at.toJwt();
|
|
121
|
+
const id = this._nextId('transport');
|
|
122
|
+
const handle = {
|
|
123
|
+
id,
|
|
124
|
+
transportId: id,
|
|
125
|
+
routerId,
|
|
126
|
+
peer: peer || null,
|
|
127
|
+
identity,
|
|
128
|
+
url: this._url,
|
|
129
|
+
token,
|
|
130
|
+
};
|
|
131
|
+
this._transports.set(id, handle);
|
|
132
|
+
this._emit('transport-new', { transportId: id, routerId, peerId: identity });
|
|
133
|
+
return handle;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async produce(transport, kind, rtpParameters)
|
|
137
|
+
{
|
|
138
|
+
if (kind !== 'audio' && kind !== 'video')
|
|
139
|
+
{
|
|
140
|
+
throw new WebRTCError('produce: kind must be "audio" or "video"', { code: 'WEBRTC_SFU_INVALID_KIND' });
|
|
141
|
+
}
|
|
142
|
+
const t = transport && this._transports.get(transport.id);
|
|
143
|
+
if (!t)
|
|
144
|
+
{
|
|
145
|
+
throw new WebRTCError('produce: unknown transport', { code: 'WEBRTC_SFU_NO_TRANSPORT' });
|
|
146
|
+
}
|
|
147
|
+
const id = this._nextId('producer');
|
|
148
|
+
const trackSid = (rtpParameters && rtpParameters.trackSid) || null;
|
|
149
|
+
const p = {
|
|
150
|
+
id, producerId: id, transportId: t.id, kind,
|
|
151
|
+
room: t.routerId, identity: t.identity, trackSid,
|
|
152
|
+
rtpParameters: rtpParameters || {}, paused: false,
|
|
153
|
+
};
|
|
154
|
+
this._producers.set(id, p);
|
|
155
|
+
this._emit('producer-new', { producerId: id, transportId: t.id, kind });
|
|
156
|
+
return p;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async consume(transport, producerId, rtpCapabilities)
|
|
160
|
+
{
|
|
161
|
+
const t = transport && this._transports.get(transport.id);
|
|
162
|
+
if (!t)
|
|
163
|
+
{
|
|
164
|
+
throw new WebRTCError('consume: unknown transport', { code: 'WEBRTC_SFU_NO_TRANSPORT' });
|
|
165
|
+
}
|
|
166
|
+
const prod = this._producers.get(producerId);
|
|
167
|
+
if (!prod)
|
|
168
|
+
{
|
|
169
|
+
throw new WebRTCError('consume: unknown producer', { code: 'WEBRTC_SFU_NO_PRODUCER' });
|
|
170
|
+
}
|
|
171
|
+
const id = this._nextId('consumer');
|
|
172
|
+
const c = {
|
|
173
|
+
id, consumerId: id, transportId: t.id, producerId,
|
|
174
|
+
kind: prod.kind, rtpParameters: prod.rtpParameters,
|
|
175
|
+
rtpCapabilities: rtpCapabilities || {},
|
|
176
|
+
};
|
|
177
|
+
this._consumers.set(id, c);
|
|
178
|
+
this._emit('consumer-new', { consumerId: id, transportId: t.id, producerId });
|
|
179
|
+
return c;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async pauseProducer(producerId)
|
|
183
|
+
{
|
|
184
|
+
const p = this._producers.get(producerId);
|
|
185
|
+
if (!p)
|
|
186
|
+
{
|
|
187
|
+
throw new WebRTCError('pauseProducer: unknown producer', { code: 'WEBRTC_SFU_NO_PRODUCER' });
|
|
188
|
+
}
|
|
189
|
+
if (p.trackSid && typeof this._client.mutePublishedTrack === 'function')
|
|
190
|
+
{
|
|
191
|
+
await this._client.mutePublishedTrack(p.room, p.identity, p.trackSid, true);
|
|
192
|
+
}
|
|
193
|
+
p.paused = true;
|
|
194
|
+
this._emit('producer-pause', { producerId });
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async resumeProducer(producerId)
|
|
198
|
+
{
|
|
199
|
+
const p = this._producers.get(producerId);
|
|
200
|
+
if (!p)
|
|
201
|
+
{
|
|
202
|
+
throw new WebRTCError('resumeProducer: unknown producer', { code: 'WEBRTC_SFU_NO_PRODUCER' });
|
|
203
|
+
}
|
|
204
|
+
if (p.trackSid && typeof this._client.mutePublishedTrack === 'function')
|
|
205
|
+
{
|
|
206
|
+
await this._client.mutePublishedTrack(p.room, p.identity, p.trackSid, false);
|
|
207
|
+
}
|
|
208
|
+
p.paused = false;
|
|
209
|
+
this._emit('producer-resume', { producerId });
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async closeRouter(routerId)
|
|
213
|
+
{
|
|
214
|
+
const room = this._rooms.get(routerId);
|
|
215
|
+
if (!room) return;
|
|
216
|
+
try { await this._client.deleteRoom(routerId); }
|
|
217
|
+
catch (err)
|
|
218
|
+
{
|
|
219
|
+
this._emit('router-close-error', { routerId, error: err && err.message });
|
|
220
|
+
}
|
|
221
|
+
// Drop every producer / consumer / transport that belonged to this room.
|
|
222
|
+
for (const [pid, p] of this._producers)
|
|
223
|
+
{
|
|
224
|
+
if (p.room === routerId)
|
|
225
|
+
{
|
|
226
|
+
this._producers.delete(pid);
|
|
227
|
+
this._emit('producer-close', { producerId: pid, reason: 'router-close' });
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
for (const [cid, c] of this._consumers)
|
|
231
|
+
{
|
|
232
|
+
const t = this._transports.get(c.transportId);
|
|
233
|
+
if (t && t.routerId === routerId)
|
|
234
|
+
{
|
|
235
|
+
this._consumers.delete(cid);
|
|
236
|
+
this._emit('consumer-close', { consumerId: cid, reason: 'router-close' });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
for (const [tid, t] of this._transports)
|
|
240
|
+
{
|
|
241
|
+
if (t.routerId === routerId)
|
|
242
|
+
{
|
|
243
|
+
this._transports.delete(tid);
|
|
244
|
+
this._emit('transport-close', { transportId: tid, reason: 'router-close' });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
this._rooms.delete(routerId);
|
|
248
|
+
this._emit('router-close', { routerId });
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async stats(scope)
|
|
252
|
+
{
|
|
253
|
+
if (scope && this._rooms.has(scope))
|
|
254
|
+
{
|
|
255
|
+
let participants = null;
|
|
256
|
+
if (typeof this._client.listParticipants === 'function')
|
|
257
|
+
{
|
|
258
|
+
try { participants = await this._client.listParticipants(scope); }
|
|
259
|
+
catch (_) { participants = null; }
|
|
260
|
+
}
|
|
261
|
+
return { kind: 'router', routerId: scope, participants };
|
|
262
|
+
}
|
|
263
|
+
if (scope && this._transports.has(scope))
|
|
264
|
+
{
|
|
265
|
+
const t = this._transports.get(scope);
|
|
266
|
+
return { kind: 'transport', transportId: scope, routerId: t.routerId, identity: t.identity };
|
|
267
|
+
}
|
|
268
|
+
let rooms = null;
|
|
269
|
+
if (typeof this._client.listRooms === 'function')
|
|
270
|
+
{
|
|
271
|
+
try { rooms = await this._client.listRooms(); }
|
|
272
|
+
catch (_) { rooms = null; }
|
|
273
|
+
}
|
|
274
|
+
return {
|
|
275
|
+
kind: 'global',
|
|
276
|
+
routers: this._rooms.size,
|
|
277
|
+
transports: this._transports.size,
|
|
278
|
+
producers: this._producers.size,
|
|
279
|
+
consumers: this._consumers.size,
|
|
280
|
+
rooms,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* @private
|
|
287
|
+
* Try to `require('livekit-server-sdk')`; throw a clean install hint when missing.
|
|
288
|
+
*/
|
|
289
|
+
function _tryRequireLivekit()
|
|
290
|
+
{
|
|
291
|
+
try { return require('livekit-server-sdk'); }
|
|
292
|
+
catch (err)
|
|
293
|
+
{
|
|
294
|
+
throw new WebRTCError(
|
|
295
|
+
"SFU adapter 'livekit' requires the 'livekit-server-sdk' peerDependency: npm install livekit-server-sdk",
|
|
296
|
+
{ code: 'WEBRTC_SFU_NOT_INSTALLED', cause: err },
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
module.exports = { LiveKitSfuAdapter };
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module webrtc/sfu/mediasoup
|
|
3
|
+
* @description mediasoup-backed SFU adapter (peerDependency on `mediasoup`).
|
|
4
|
+
*
|
|
5
|
+
* Wraps a single mediasoup `Worker` and one `Router` per createRouter()
|
|
6
|
+
* call. WebRTC transports are created with `router.createWebRtcTransport()`
|
|
7
|
+
* and produce / consume / pause / resume / close / stats all delegate to
|
|
8
|
+
* the native mediasoup objects.
|
|
9
|
+
*
|
|
10
|
+
* `mediasoup` is loaded lazily. Tests inject a stub via `opts.mediasoup`;
|
|
11
|
+
* in production the constructor `require('mediasoup')`s the real package
|
|
12
|
+
* and throws `WEBRTC_SFU_NOT_INSTALLED` if it is missing.
|
|
13
|
+
*/
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
const { SfuAdapter } = require('./index');
|
|
17
|
+
const { WebRTCError } = require('../../errors');
|
|
18
|
+
|
|
19
|
+
const DEFAULT_MEDIA_CODECS = [
|
|
20
|
+
{ kind: 'audio', mimeType: 'audio/opus', clockRate: 48000, channels: 2 },
|
|
21
|
+
{ kind: 'video', mimeType: 'video/VP8', clockRate: 90000 },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const DEFAULT_WEBRTC_TRANSPORT_OPTS = {
|
|
25
|
+
listenIps: [{ ip: '0.0.0.0', announcedIp: null }],
|
|
26
|
+
enableUdp: true,
|
|
27
|
+
enableTcp: true,
|
|
28
|
+
preferUdp: true,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
class MediasoupSfuAdapter extends SfuAdapter
|
|
32
|
+
{
|
|
33
|
+
/**
|
|
34
|
+
* @param {object} [opts]
|
|
35
|
+
* @param {object} [opts.mediasoup] Injected mediasoup module (testing); defaults to `require('mediasoup')`.
|
|
36
|
+
* @param {object} [opts.worker] Pre-created `mediasoup.Worker`; bypasses the lazy worker bootstrap.
|
|
37
|
+
* @param {object} [opts.workerSettings] Forwarded to `mediasoup.createWorker(...)`.
|
|
38
|
+
* @param {Array} [opts.mediaCodecs] Default router media codecs.
|
|
39
|
+
* @param {object} [opts.webRtcTransportOptions] Default `router.createWebRtcTransport(...)` options.
|
|
40
|
+
*/
|
|
41
|
+
constructor(opts)
|
|
42
|
+
{
|
|
43
|
+
super();
|
|
44
|
+
const o = opts || {};
|
|
45
|
+
|
|
46
|
+
this._mediasoup = o.mediasoup || _tryRequireMediasoup();
|
|
47
|
+
this._workerSettings = o.workerSettings || {};
|
|
48
|
+
this._mediaCodecs = o.mediaCodecs || DEFAULT_MEDIA_CODECS;
|
|
49
|
+
this._webRtcTransportOpts = o.webRtcTransportOptions || DEFAULT_WEBRTC_TRANSPORT_OPTS;
|
|
50
|
+
this._worker = o.worker || null;
|
|
51
|
+
this._workerPromise = null;
|
|
52
|
+
|
|
53
|
+
this._routers = new Map(); // routerId -> native router
|
|
54
|
+
this._transports = new Map(); // transportId -> native transport
|
|
55
|
+
this._producers = new Map(); // producerId -> native producer
|
|
56
|
+
this._consumers = new Map(); // consumerId -> native consumer
|
|
57
|
+
this._routerOf = new Map(); // transportId -> routerId
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Lazily create (or return) the single shared mediasoup Worker.
|
|
62
|
+
* Returns the native Worker handle.
|
|
63
|
+
*/
|
|
64
|
+
async _ensureWorker()
|
|
65
|
+
{
|
|
66
|
+
if (this._worker) return this._worker;
|
|
67
|
+
if (!this._workerPromise)
|
|
68
|
+
{
|
|
69
|
+
this._workerPromise = Promise.resolve(this._mediasoup.createWorker(this._workerSettings))
|
|
70
|
+
.then((w) =>
|
|
71
|
+
{
|
|
72
|
+
this._worker = w;
|
|
73
|
+
if (typeof w.on === 'function')
|
|
74
|
+
{
|
|
75
|
+
w.on('died', (err) => this._emit('worker-died', { error: err && err.message }));
|
|
76
|
+
}
|
|
77
|
+
return w;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return this._workerPromise;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async createRouter(opts)
|
|
84
|
+
{
|
|
85
|
+
const worker = await this._ensureWorker();
|
|
86
|
+
const mediaCodecs = (opts && opts.mediaCodecs) || this._mediaCodecs;
|
|
87
|
+
const router = await worker.createRouter({ mediaCodecs });
|
|
88
|
+
this._routers.set(router.id, router);
|
|
89
|
+
if (typeof router.observer === 'object' && router.observer && typeof router.observer.on === 'function')
|
|
90
|
+
{
|
|
91
|
+
router.observer.on('close', () =>
|
|
92
|
+
{
|
|
93
|
+
this._routers.delete(router.id);
|
|
94
|
+
this._emit('router-close', { routerId: router.id });
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
this._emit('router-new', { routerId: router.id });
|
|
98
|
+
return {
|
|
99
|
+
id: router.id,
|
|
100
|
+
routerId: router.id,
|
|
101
|
+
rtpCapabilities: router.rtpCapabilities,
|
|
102
|
+
_native: router,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async createTransport(router, peer)
|
|
107
|
+
{
|
|
108
|
+
const routerId = router && router.id;
|
|
109
|
+
const native = routerId && this._routers.get(routerId);
|
|
110
|
+
if (!native)
|
|
111
|
+
{
|
|
112
|
+
throw new WebRTCError('createTransport: unknown router', { code: 'WEBRTC_SFU_NO_ROUTER' });
|
|
113
|
+
}
|
|
114
|
+
const transport = await native.createWebRtcTransport({
|
|
115
|
+
...this._webRtcTransportOpts,
|
|
116
|
+
appData: { peer: peer || null },
|
|
117
|
+
});
|
|
118
|
+
this._transports.set(transport.id, transport);
|
|
119
|
+
this._routerOf.set(transport.id, routerId);
|
|
120
|
+
if (typeof transport.observer === 'object' && transport.observer && typeof transport.observer.on === 'function')
|
|
121
|
+
{
|
|
122
|
+
transport.observer.on('close', () =>
|
|
123
|
+
{
|
|
124
|
+
this._transports.delete(transport.id);
|
|
125
|
+
this._routerOf.delete(transport.id);
|
|
126
|
+
this._emit('transport-close', { transportId: transport.id });
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
this._emit('transport-new', { transportId: transport.id, routerId, peerId: peer && peer.id });
|
|
130
|
+
return {
|
|
131
|
+
id: transport.id,
|
|
132
|
+
transportId: transport.id,
|
|
133
|
+
routerId,
|
|
134
|
+
peer: peer || null,
|
|
135
|
+
iceParameters: transport.iceParameters,
|
|
136
|
+
iceCandidates: transport.iceCandidates,
|
|
137
|
+
dtlsParameters: transport.dtlsParameters,
|
|
138
|
+
sctpParameters: transport.sctpParameters || null,
|
|
139
|
+
_native: transport,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async produce(transport, kind, rtpParameters)
|
|
144
|
+
{
|
|
145
|
+
if (kind !== 'audio' && kind !== 'video')
|
|
146
|
+
{
|
|
147
|
+
throw new WebRTCError('produce: kind must be "audio" or "video"', { code: 'WEBRTC_SFU_INVALID_KIND' });
|
|
148
|
+
}
|
|
149
|
+
const native = transport && this._transports.get(transport.id);
|
|
150
|
+
if (!native)
|
|
151
|
+
{
|
|
152
|
+
throw new WebRTCError('produce: unknown transport', { code: 'WEBRTC_SFU_NO_TRANSPORT' });
|
|
153
|
+
}
|
|
154
|
+
const producer = await native.produce({ kind, rtpParameters });
|
|
155
|
+
this._producers.set(producer.id, producer);
|
|
156
|
+
if (typeof producer.on === 'function')
|
|
157
|
+
{
|
|
158
|
+
producer.on('transportclose', () =>
|
|
159
|
+
{
|
|
160
|
+
this._producers.delete(producer.id);
|
|
161
|
+
this._emit('producer-close', { producerId: producer.id, reason: 'transport-close' });
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
this._emit('producer-new', { producerId: producer.id, transportId: transport.id, kind });
|
|
165
|
+
return {
|
|
166
|
+
id: producer.id,
|
|
167
|
+
producerId: producer.id,
|
|
168
|
+
transportId: transport.id,
|
|
169
|
+
kind,
|
|
170
|
+
rtpParameters,
|
|
171
|
+
paused: !!producer.paused,
|
|
172
|
+
_native: producer,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async consume(transport, producerId, rtpCapabilities)
|
|
177
|
+
{
|
|
178
|
+
const native = transport && this._transports.get(transport.id);
|
|
179
|
+
if (!native)
|
|
180
|
+
{
|
|
181
|
+
throw new WebRTCError('consume: unknown transport', { code: 'WEBRTC_SFU_NO_TRANSPORT' });
|
|
182
|
+
}
|
|
183
|
+
const routerId = this._routerOf.get(transport.id);
|
|
184
|
+
const router = routerId && this._routers.get(routerId);
|
|
185
|
+
if (router && typeof router.canConsume === 'function'
|
|
186
|
+
&& !router.canConsume({ producerId, rtpCapabilities }))
|
|
187
|
+
{
|
|
188
|
+
throw new WebRTCError('consume: router cannot consume producer with given rtpCapabilities',
|
|
189
|
+
{ code: 'WEBRTC_SFU_CANNOT_CONSUME' });
|
|
190
|
+
}
|
|
191
|
+
let consumer;
|
|
192
|
+
try
|
|
193
|
+
{
|
|
194
|
+
consumer = await native.consume({ producerId, rtpCapabilities });
|
|
195
|
+
}
|
|
196
|
+
catch (err)
|
|
197
|
+
{
|
|
198
|
+
throw new WebRTCError(`consume failed: ${err.message}`, { code: 'WEBRTC_SFU_CONSUME_FAILED', cause: err });
|
|
199
|
+
}
|
|
200
|
+
this._consumers.set(consumer.id, consumer);
|
|
201
|
+
if (typeof consumer.on === 'function')
|
|
202
|
+
{
|
|
203
|
+
consumer.on('transportclose', () =>
|
|
204
|
+
{
|
|
205
|
+
this._consumers.delete(consumer.id);
|
|
206
|
+
this._emit('consumer-close', { consumerId: consumer.id, reason: 'transport-close' });
|
|
207
|
+
});
|
|
208
|
+
consumer.on('producerclose', () =>
|
|
209
|
+
{
|
|
210
|
+
this._consumers.delete(consumer.id);
|
|
211
|
+
this._emit('consumer-close', { consumerId: consumer.id, reason: 'producer-close' });
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
this._emit('consumer-new', { consumerId: consumer.id, transportId: transport.id, producerId });
|
|
215
|
+
return {
|
|
216
|
+
id: consumer.id,
|
|
217
|
+
consumerId: consumer.id,
|
|
218
|
+
transportId: transport.id,
|
|
219
|
+
producerId,
|
|
220
|
+
kind: consumer.kind,
|
|
221
|
+
rtpParameters: consumer.rtpParameters,
|
|
222
|
+
rtpCapabilities,
|
|
223
|
+
_native: consumer,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async pauseProducer(producerId)
|
|
228
|
+
{
|
|
229
|
+
const p = this._producers.get(producerId);
|
|
230
|
+
if (!p)
|
|
231
|
+
{
|
|
232
|
+
throw new WebRTCError('pauseProducer: unknown producer', { code: 'WEBRTC_SFU_NO_PRODUCER' });
|
|
233
|
+
}
|
|
234
|
+
await p.pause();
|
|
235
|
+
this._emit('producer-pause', { producerId });
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async resumeProducer(producerId)
|
|
239
|
+
{
|
|
240
|
+
const p = this._producers.get(producerId);
|
|
241
|
+
if (!p)
|
|
242
|
+
{
|
|
243
|
+
throw new WebRTCError('resumeProducer: unknown producer', { code: 'WEBRTC_SFU_NO_PRODUCER' });
|
|
244
|
+
}
|
|
245
|
+
await p.resume();
|
|
246
|
+
this._emit('producer-resume', { producerId });
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async closeRouter(routerId)
|
|
250
|
+
{
|
|
251
|
+
const r = this._routers.get(routerId);
|
|
252
|
+
if (!r) return;
|
|
253
|
+
// Native router.close() cascades to its transports; the observer
|
|
254
|
+
// 'close' handlers we registered in createTransport/createRouter
|
|
255
|
+
// emit transport-close / router-close events for us. Avoid
|
|
256
|
+
// emitting here to prevent duplicates.
|
|
257
|
+
await r.close();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async stats(scope)
|
|
261
|
+
{
|
|
262
|
+
if (scope && this._routers.has(scope))
|
|
263
|
+
{
|
|
264
|
+
const r = this._routers.get(scope);
|
|
265
|
+
const native = typeof r.getStats === 'function' ? await r.getStats() : null;
|
|
266
|
+
return { kind: 'router', routerId: scope, native };
|
|
267
|
+
}
|
|
268
|
+
if (scope && this._transports.has(scope))
|
|
269
|
+
{
|
|
270
|
+
const t = this._transports.get(scope);
|
|
271
|
+
const native = typeof t.getStats === 'function' ? await t.getStats() : null;
|
|
272
|
+
return { kind: 'transport', transportId: scope, routerId: this._routerOf.get(scope), native };
|
|
273
|
+
}
|
|
274
|
+
return {
|
|
275
|
+
kind: 'global',
|
|
276
|
+
routers: this._routers.size,
|
|
277
|
+
transports: this._transports.size,
|
|
278
|
+
producers: this._producers.size,
|
|
279
|
+
consumers: this._consumers.size,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Best-effort shutdown: closes every router, then the worker if we own it.
|
|
285
|
+
*/
|
|
286
|
+
async close()
|
|
287
|
+
{
|
|
288
|
+
for (const id of [...this._routers.keys()])
|
|
289
|
+
{
|
|
290
|
+
try { await this.closeRouter(id); } catch (_) { /* swallow */ }
|
|
291
|
+
}
|
|
292
|
+
if (this._worker && typeof this._worker.close === 'function')
|
|
293
|
+
{
|
|
294
|
+
try { await this._worker.close(); } catch (_) { /* swallow */ }
|
|
295
|
+
}
|
|
296
|
+
this._worker = null;
|
|
297
|
+
this._workerPromise = null;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* @private
|
|
303
|
+
* Try to `require('mediasoup')`; throw a clean install hint when missing.
|
|
304
|
+
*/
|
|
305
|
+
function _tryRequireMediasoup()
|
|
306
|
+
{
|
|
307
|
+
try { return require('mediasoup'); }
|
|
308
|
+
catch (err)
|
|
309
|
+
{
|
|
310
|
+
throw new WebRTCError(
|
|
311
|
+
"SFU adapter 'mediasoup' requires the 'mediasoup' peerDependency: npm install mediasoup",
|
|
312
|
+
{ code: 'WEBRTC_SFU_NOT_INSTALLED', cause: err },
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
module.exports = { MediasoupSfuAdapter };
|