@fishjam-cloud/ts-client 0.25.1 → 0.25.2

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.
Files changed (37) hide show
  1. package/dist/index.d.mts +10 -0
  2. package/dist/index.mjs +1 -1
  3. package/dist/index.react-native.mjs +17 -0
  4. package/dist/protobufs/fishjam/media_events/peer/peer.d.ts +130 -0
  5. package/dist/protobufs/fishjam/media_events/peer/peer.js +1283 -0
  6. package/dist/protobufs/fishjam/media_events/server/server.d.ts +197 -0
  7. package/dist/protobufs/fishjam/media_events/server/server.js +2121 -0
  8. package/dist/protobufs/fishjam/media_events/shared.d.ts +48 -0
  9. package/dist/protobufs/fishjam/media_events/shared.js +315 -0
  10. package/dist/protobufs/fishjam/peer_notifications.d.ts +83 -0
  11. package/dist/protobufs/fishjam/peer_notifications.js +559 -0
  12. package/dist/ts-client/package.json +82 -0
  13. package/dist/ts-client/src/FishjamClient.d.ts +438 -0
  14. package/dist/ts-client/src/FishjamClient.js +821 -0
  15. package/dist/ts-client/src/auth.d.ts +3 -0
  16. package/dist/ts-client/src/auth.js +8 -0
  17. package/dist/ts-client/src/connectEventsHandler.d.ts +2 -0
  18. package/dist/ts-client/src/connectEventsHandler.js +22 -0
  19. package/dist/ts-client/src/errors.d.ts +3 -0
  20. package/dist/ts-client/src/errors.js +5 -0
  21. package/dist/ts-client/src/guards.d.ts +7 -0
  22. package/dist/ts-client/src/guards.js +8 -0
  23. package/dist/ts-client/src/index.d.ts +9 -0
  24. package/dist/ts-client/src/index.js +6 -0
  25. package/dist/ts-client/src/livestream.d.ts +20 -0
  26. package/dist/ts-client/src/livestream.js +78 -0
  27. package/dist/ts-client/src/messageQueue.d.ts +13 -0
  28. package/dist/ts-client/src/messageQueue.js +23 -0
  29. package/dist/ts-client/src/reconnection.d.ts +27 -0
  30. package/dist/ts-client/src/reconnection.js +124 -0
  31. package/dist/ts-client/src/tests/messageQueue.test.d.ts +1 -0
  32. package/dist/ts-client/src/tests/messageQueue.test.js +54 -0
  33. package/dist/ts-client/src/types.d.ts +216 -0
  34. package/dist/ts-client/src/types.js +1 -0
  35. package/dist/ts-client/src/version.d.ts +1 -0
  36. package/dist/ts-client/src/version.js +2 -0
  37. package/package.json +18 -11
@@ -0,0 +1,821 @@
1
+ import { PeerMessage, PeerMessage_RoomType, PeerMessage_SdkDeprecation_Status, } from '@fishjam-cloud/protobufs/fishjamPeer';
2
+ import { MediaEvent as PeerMediaEvent } from '@fishjam-cloud/protobufs/peer';
3
+ import { MediaEvent as ServerMediaEvent } from '@fishjam-cloud/protobufs/server';
4
+ import { ChannelMessage } from '@fishjam-cloud/protobufs/shared';
5
+ import { getLogger, WebRTCEndpoint } from '@fishjam-cloud/webrtc-client';
6
+ import { EventEmitter } from 'events';
7
+ import { isAuthError } from './auth';
8
+ import { connectEventsHandler } from './connectEventsHandler';
9
+ import { TrackTypeError } from './errors';
10
+ import { isComponent, isJoinError, isPeer } from './guards';
11
+ import { MessageQueue } from './messageQueue';
12
+ import { ReconnectManager } from './reconnection';
13
+ import { packageVersion } from './version';
14
+ const STATISTICS_INTERVAL = 10_000;
15
+ const WEBSOCKET_PATH = 'socket/peer/websocket';
16
+ /**
17
+ * FishjamClient is the main class to interact with Fishjam.
18
+ *
19
+ * @example
20
+ * ```typescript
21
+ * const client = new FishjamClient<PeerMetadata>();
22
+ * const peerToken = "YOUR_PEER_TOKEN";
23
+ *
24
+ * // You can listen to events emitted by the client
25
+ * client.on("joined", (peerId, peersInRoom) => {
26
+ * console.log("join success");
27
+ * });
28
+ *
29
+ * // Start the peer connection
30
+ * client.connect({
31
+ * peerMetadata: {},
32
+ * isSimulcastOn: false,
33
+ * token: peerToken
34
+ * });
35
+ *
36
+ * // Close the peer connection
37
+ * client.disconnect();
38
+ * ```
39
+ *
40
+ * You can register callbacks to handle the events emitted by the Client.
41
+ *
42
+ * @example
43
+ * ```typescript
44
+ *
45
+ * client.on("trackReady", (ctx) => {
46
+ * console.log("On track ready");
47
+ * });
48
+ * ```
49
+ */
50
+ export class FishjamClient extends EventEmitter {
51
+ websocket = null;
52
+ webrtc = null;
53
+ removeEventListeners = null;
54
+ debug;
55
+ logger;
56
+ clientType;
57
+ status = 'new';
58
+ connectConfig = null;
59
+ isAudioOnlyConnection = false;
60
+ reconnectManager;
61
+ peerMessageQueue;
62
+ sendStatisticsInterval = undefined;
63
+ constructor(config) {
64
+ super();
65
+ this.debug = !!config?.debug;
66
+ this.logger = getLogger(this.debug);
67
+ this.clientType = config?.clientType ?? 'web';
68
+ this.reconnectManager = new ReconnectManager(this, (peerMetadata) => this.initConnection(peerMetadata), config?.reconnect ?? true);
69
+ this.peerMessageQueue = new MessageQueue({
70
+ sendMessage: (event) => this.websocket?.send(event),
71
+ checkIsReconnecting: () => this.reconnectManager.isReconnecting(),
72
+ });
73
+ this.on('reconnected', () => this.peerMessageQueue.attemptToSendAll());
74
+ }
75
+ /**
76
+ * Uses the WebSocket connection and {@link !WebRTCEndpoint | WebRTCEndpoint} to join to the room. Registers the callbacks to
77
+ * handle the events emitted by the {@link !WebRTCEndpoint | WebRTCEndpoint}. Make sure that peer metadata is serializable.
78
+ *
79
+ * @example
80
+ * ```typescript
81
+ * const client = new FishjamClient<PeerMetadata>();
82
+ *
83
+ * client.connect({
84
+ * peerMetadata: {},
85
+ * token: peerToken
86
+ * });
87
+ * ```
88
+ *
89
+ * @param {ConnectConfig} config - Configuration object for the client
90
+ */
91
+ async connect(config) {
92
+ this.emit('connectionStarted');
93
+ const result = connectEventsHandler(this);
94
+ this.reconnectManager.reset(config.peerMetadata);
95
+ this.connectConfig = config;
96
+ this.initConnection(config.peerMetadata);
97
+ return result;
98
+ }
99
+ async initConnection(peerMetadata) {
100
+ if (this.status === 'initialized') {
101
+ this.disconnect();
102
+ }
103
+ this.webrtc = new WebRTCEndpoint({
104
+ debug: this.debug,
105
+ });
106
+ this.initWebsocket(peerMetadata);
107
+ this.setupCallbacks();
108
+ this.status = 'initialized';
109
+ }
110
+ getUrl(url) {
111
+ if (url.endsWith('/'))
112
+ return `${url}${WEBSOCKET_PATH}`;
113
+ return `${url}/${WEBSOCKET_PATH}`;
114
+ }
115
+ initWebsocket(peerMetadata) {
116
+ if (!this.connectConfig)
117
+ throw Error('ConnectConfig is null');
118
+ const { token, url } = this.connectConfig;
119
+ const websocketUrl = this.getUrl(url);
120
+ this.websocket = new WebSocket(websocketUrl);
121
+ this.websocket.binaryType = 'arraybuffer';
122
+ const socketOpenHandler = (event) => {
123
+ this.emit('socketOpen', event);
124
+ const sdkVersion = `${this.clientType}-${packageVersion}`;
125
+ const message = PeerMessage.encode({ authRequest: { token, sdkVersion } }).finish();
126
+ this.websocket?.send(message);
127
+ };
128
+ const socketErrorHandler = (event) => {
129
+ this.emit('socketError', event);
130
+ };
131
+ const socketCloseHandler = (event) => {
132
+ if (isAuthError(event.reason)) {
133
+ this.emit('authError', event.reason);
134
+ }
135
+ if (isJoinError(event.reason)) {
136
+ this.emit('joinError', event.reason);
137
+ }
138
+ this.logger.warn(`Socket closed with reason: ${event.reason}`);
139
+ this.emit('socketClose', event);
140
+ };
141
+ this.websocket.addEventListener('open', socketOpenHandler);
142
+ this.websocket.addEventListener('error', socketErrorHandler);
143
+ this.websocket.addEventListener('close', socketCloseHandler);
144
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
145
+ const messageHandler = (event) => {
146
+ const uint8Array = new Uint8Array(event.data);
147
+ try {
148
+ const data = PeerMessage.decode(uint8Array);
149
+ const serverMediaEvent = data.serverMediaEvent;
150
+ if (data.authenticated) {
151
+ this.isAudioOnlyConnection = data.authenticated.roomType === PeerMessage_RoomType.ROOM_TYPE_AUDIO_ONLY;
152
+ if (data.authenticated.sdkDeprecation) {
153
+ this.handleSdkDeprecation(data.authenticated.sdkDeprecation);
154
+ }
155
+ this.emit('authSuccess');
156
+ this.webrtc?.connect(peerMetadata);
157
+ }
158
+ else if (data.authRequest) {
159
+ this.logger.warn('Received unexpected control message: authRequest');
160
+ }
161
+ else if (serverMediaEvent) {
162
+ this.webrtc?.receiveMediaEvent(ServerMediaEvent.encode(serverMediaEvent).finish());
163
+ }
164
+ }
165
+ catch (e) {
166
+ this.logger.warn(`Received invalid control message, error: ${e}`);
167
+ }
168
+ };
169
+ this.websocket.addEventListener('message', messageHandler);
170
+ this.removeEventListeners = () => {
171
+ this.websocket?.removeEventListener('open', socketOpenHandler);
172
+ this.websocket?.removeEventListener('error', socketErrorHandler);
173
+ this.websocket?.removeEventListener('close', socketCloseHandler);
174
+ this.websocket?.removeEventListener('message', messageHandler);
175
+ };
176
+ }
177
+ handleSdkDeprecation(sdkDeprecation) {
178
+ switch (sdkDeprecation.status) {
179
+ case PeerMessage_SdkDeprecation_Status.STATUS_UNSUPPORTED:
180
+ this.logger.error(sdkDeprecation.message);
181
+ break;
182
+ case PeerMessage_SdkDeprecation_Status.STATUS_DEPRECATED:
183
+ this.logger.warn(sdkDeprecation.message);
184
+ break;
185
+ default:
186
+ break;
187
+ }
188
+ }
189
+ /**
190
+ * Retrieves statistics related to the RTCPeerConnection.
191
+ * These statistics provide insights into the performance and status of the connection.
192
+ *
193
+ * @return {Promise<RTCStatsReport>}
194
+ *
195
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/getStats | MDN Web Docs: RTCPeerConnection.getStats()}
196
+ */
197
+ async getStatistics(selector) {
198
+ return (await this.webrtc?.getStatistics(selector)) ?? new Map();
199
+ }
200
+ /**
201
+ * Returns a snapshot of currently received remote tracks.
202
+ *
203
+ * @example
204
+ * if (client.getRemoteTracks()[trackId]?.simulcastConfig?.enabled) {
205
+ * client.setTargetTrackEncoding(trackId, encoding);
206
+ * }
207
+ */
208
+ getRemoteTracks() {
209
+ return this.webrtc?.getRemoteTracks() ?? {};
210
+ }
211
+ /**
212
+ * Returns a snapshot of currently received remote peers.
213
+ */
214
+ getRemotePeers() {
215
+ return Object.entries(this.webrtc?.getRemoteEndpoints() ?? {}).reduce((acc, [id, peer]) => (isPeer(peer) ? { ...acc, [id]: peer } : acc), {});
216
+ }
217
+ getRemoteComponents() {
218
+ return Object.entries(this.webrtc?.getRemoteEndpoints() ?? {}).reduce((acc, [id, component]) => (isComponent(component) ? { ...acc, [id]: component } : acc), {});
219
+ }
220
+ getLocalPeer() {
221
+ return this.webrtc?.getLocalEndpoint() || null;
222
+ }
223
+ getBandwidthEstimation() {
224
+ if (!this.webrtc)
225
+ throw Error('Webrtc not initialized');
226
+ return this.webrtc?.getBandwidthEstimation();
227
+ }
228
+ isConnectingRelatedEvent(mediaEvent) {
229
+ return Boolean(mediaEvent.connect || mediaEvent.renegotiateTracks || mediaEvent.sdpOffer || mediaEvent.candidate);
230
+ }
231
+ setupCallbacks() {
232
+ this.webrtc?.on('sendMediaEvent', (mediaEvent) => {
233
+ const peerMediaEvent = PeerMediaEvent.decode(mediaEvent);
234
+ const serializedMessage = PeerMessage.encode({ peerMediaEvent }).finish();
235
+ if (this.isConnectingRelatedEvent(peerMediaEvent)) {
236
+ this.websocket?.send(serializedMessage);
237
+ }
238
+ else {
239
+ this.peerMessageQueue.enqueueMessage(serializedMessage);
240
+ }
241
+ });
242
+ this.webrtc?.on('connected', async (peerId, endpointsInRoom) => {
243
+ const peers = endpointsInRoom
244
+ .filter((endpoint) => isPeer(endpoint))
245
+ .map((peer) => peer);
246
+ const components = endpointsInRoom
247
+ .filter((endpoint) => isComponent(endpoint))
248
+ .map((component) => component);
249
+ await this.reconnectManager.handleReconnect();
250
+ this.sendStatisticsInterval = setInterval(() => this.sendStatistics(), STATISTICS_INTERVAL);
251
+ this.emit('joined', peerId, peers, components);
252
+ });
253
+ this.webrtc?.on('disconnected', () => {
254
+ this.emit('disconnected');
255
+ clearInterval(this.sendStatisticsInterval);
256
+ });
257
+ this.webrtc?.on('endpointAdded', (endpoint) => {
258
+ if (isPeer(endpoint)) {
259
+ this.emit('peerJoined', endpoint);
260
+ }
261
+ if (isComponent(endpoint)) {
262
+ this.emit('componentAdded', endpoint);
263
+ }
264
+ });
265
+ this.webrtc?.on('endpointRemoved', (endpoint) => {
266
+ if (isPeer(endpoint)) {
267
+ this.emit('peerLeft', endpoint);
268
+ }
269
+ if (isComponent(endpoint)) {
270
+ this.emit('componentRemoved', endpoint);
271
+ }
272
+ });
273
+ this.webrtc?.on('endpointUpdated', (endpoint) => {
274
+ if (isPeer(endpoint)) {
275
+ this.emit('peerUpdated', endpoint);
276
+ }
277
+ if (isComponent(endpoint)) {
278
+ this.emit('componentUpdated', endpoint);
279
+ }
280
+ });
281
+ this.webrtc?.on('trackReady', (ctx) => {
282
+ if (!isPeer(ctx.endpoint))
283
+ return;
284
+ this.emit('trackReady', ctx);
285
+ });
286
+ this.webrtc?.on('trackAdded', (ctx) => {
287
+ if (!isPeer(ctx.endpoint))
288
+ return;
289
+ this.emit('trackAdded', ctx);
290
+ });
291
+ this.webrtc?.on('trackRemoved', (ctx) => {
292
+ if (!isPeer(ctx.endpoint))
293
+ return;
294
+ this.emit('trackRemoved', ctx);
295
+ ctx.removeAllListeners();
296
+ });
297
+ this.webrtc?.on('trackUpdated', (ctx) => {
298
+ if (!isPeer(ctx.endpoint))
299
+ return;
300
+ this.emit('trackUpdated', ctx);
301
+ });
302
+ this.webrtc?.on('tracksPriorityChanged', (enabledTracks, disabledTracks) => {
303
+ this.emit('tracksPriorityChanged', enabledTracks, disabledTracks);
304
+ });
305
+ this.webrtc?.on('signalingError', (error) => {
306
+ this.emit('joinError', error);
307
+ });
308
+ this.webrtc?.on('connectionError', (error) => {
309
+ this.emit('connectionError', error);
310
+ });
311
+ this.webrtc?.on('bandwidthEstimationChanged', (estimation) => {
312
+ this.emit('bandwidthEstimationChanged', estimation);
313
+ });
314
+ this.webrtc?.on('targetTrackEncodingRequested', (event) => {
315
+ this.emit('targetTrackEncodingRequested', event);
316
+ });
317
+ this.webrtc?.on('localTrackAdded', (event) => {
318
+ this.emit('localTrackAdded', event);
319
+ });
320
+ this.webrtc?.on('localTrackRemoved', (event) => {
321
+ this.emit('localTrackRemoved', event);
322
+ });
323
+ this.webrtc?.on('localTrackReplaced', (event) => {
324
+ this.emit('localTrackReplaced', event);
325
+ });
326
+ this.webrtc?.on('localTrackBandwidthSet', (event) => {
327
+ this.emit('localTrackBandwidthSet', event);
328
+ });
329
+ this.webrtc?.on('localTrackMuted', (event) => {
330
+ this.emit('localTrackMuted', event);
331
+ });
332
+ this.webrtc?.on('localTrackUnmuted', (event) => {
333
+ this.emit('localTrackUnmuted', event);
334
+ });
335
+ this.webrtc?.on('localTrackBandwidthSet', (event) => {
336
+ this.emit('localTrackBandwidthSet', event);
337
+ });
338
+ this.webrtc?.on('localTrackEncodingEnabled', (event) => {
339
+ this.emit('localTrackEncodingEnabled', event);
340
+ });
341
+ this.webrtc?.on('localTrackEncodingDisabled', (event) => {
342
+ this.emit('localTrackEncodingDisabled', event);
343
+ });
344
+ this.webrtc?.on('localEndpointMetadataChanged', (event) => {
345
+ this.emit('localPeerMetadataChanged', event);
346
+ });
347
+ this.webrtc?.on('localTrackMetadataChanged', (event) => {
348
+ this.emit('localTrackMetadataChanged', event);
349
+ });
350
+ this.webrtc?.on('disconnectRequested', (event) => {
351
+ this.emit('disconnectRequested', event);
352
+ });
353
+ this.webrtc?.on('dataChannelsReady', () => {
354
+ this.emit('dataChannelsReady');
355
+ });
356
+ this.webrtc?.on('dataChannelsError', (error) => {
357
+ this.emit('dataChannelsError', error);
358
+ });
359
+ }
360
+ async sendStatistics() {
361
+ const statistics = await this.getStatistics();
362
+ const tracksStatistics = {};
363
+ statistics.forEach((report, key) => {
364
+ if (report.type === 'inbound-rtp' || report.type === 'outbound-rtp')
365
+ tracksStatistics[key] = report;
366
+ });
367
+ const message = PeerMessage.encode({
368
+ rtcStatsReport: { data: JSON.stringify(tracksStatistics) },
369
+ }).finish();
370
+ this.peerMessageQueue.enqueueMessage(message);
371
+ }
372
+ /**
373
+ * Register a callback to be called when the event is emitted.
374
+ * Full list of callbacks can be found here {@link MessageEvents}.
375
+ *
376
+ * @example
377
+ * ```ts
378
+ * const callback = ()=>{ };
379
+ *
380
+ * client.on("onJoinSuccess", callback);
381
+ * ```
382
+ *
383
+ * @param event - Event name from {@link MessageEvents}
384
+ * @param listener - Callback function to be called when the event is emitted
385
+ * @returns This
386
+ */
387
+ on(event, listener) {
388
+ return super.on(event, listener);
389
+ }
390
+ /**
391
+ * Remove a callback from the list of callbacks to be called when the event is emitted.
392
+ *
393
+ * @example
394
+ * ```ts
395
+ * const callback = ()=>{ };
396
+ *
397
+ * client.on("onJoinSuccess", callback);
398
+ *
399
+ * client.off("onJoinSuccess", callback);
400
+ * ```
401
+ *
402
+ * @param event - Event name from {@link MessageEvents}
403
+ * @param listener - Reference to function to be removed from called callbacks
404
+ * @returns This
405
+ */
406
+ off(event, listener) {
407
+ return super.off(event, listener);
408
+ }
409
+ handleWebRTCNotInitialized() {
410
+ return new Error('WebRTC is not initialized');
411
+ }
412
+ /**
413
+ * Adds track that will be sent to the RTC Engine.
414
+ *
415
+ * @example
416
+ * ```ts
417
+ * const localStream: MediaStream = new MediaStream();
418
+ * try {
419
+ * const localAudioStream = await navigator.mediaDevices.getUserMedia(
420
+ * { audio: true }
421
+ * );
422
+ * localAudioStream
423
+ * .getTracks()
424
+ * .forEach((track) => localStream.addTrack(track));
425
+ * } catch (error) {
426
+ * console.error("Couldn't get microphone permission:", error);
427
+ * }
428
+ *
429
+ * try {
430
+ * const localVideoStream = await navigator.mediaDevices.getUserMedia(
431
+ * { video: true }
432
+ * );
433
+ * localVideoStream
434
+ * .getTracks()
435
+ * .forEach((track) => localStream.addTrack(track));
436
+ * } catch (error) {
437
+ * console.error("Couldn't get camera permission:", error);
438
+ * }
439
+ *
440
+ * localStream
441
+ * .getTracks()
442
+ * .forEach((track) => client.addTrack(track, localStream));
443
+ * ```
444
+ *
445
+ * @param track - Audio or video track e.g. from your microphone or camera.
446
+ * @param trackMetadata - Any information about this track that other peers will receive in
447
+ * {@link MessageEvents.peerJoined}. E.g. this can source of the track - wheather it's screensharing, webcam or some
448
+ * other media device.
449
+ * @param simulcastConfig - Simulcast configuration. By default, simulcast is disabled. For more information refer to
450
+ * {@link !SimulcastConfig | SimulcastConfig}.
451
+ * @param maxBandwidth - Maximal bandwidth this track can use. Defaults to 0 which is unlimited. This option has no
452
+ * effect for simulcast and audio tracks. For simulcast tracks use {@link FishjamClient.setTrackBandwidth}.
453
+ * @returns {string} Returns id of added track
454
+ */
455
+ addTrack(track, trackMetadata, simulcastConfig = {
456
+ enabled: false,
457
+ enabledVariants: [],
458
+ disabledVariants: [],
459
+ }, maxBandwidth = 0) {
460
+ if (!this.webrtc)
461
+ throw this.handleWebRTCNotInitialized();
462
+ if (this.isAudioOnlyConnection && track.kind !== 'audio')
463
+ throw new TrackTypeError();
464
+ return this.webrtc.addTrack(track, trackMetadata, simulcastConfig, maxBandwidth);
465
+ }
466
+ /**
467
+ * Replaces a track that is being sent to the RTC Engine.
468
+ *
469
+ * @example
470
+ * ```ts
471
+ * // setup camera
472
+ * let localStream: MediaStream = new MediaStream();
473
+ * try {
474
+ * localVideoStream = await navigator.mediaDevices.getUserMedia(
475
+ * VIDEO_CONSTRAINTS
476
+ * );
477
+ * localVideoStream
478
+ * .getTracks()
479
+ * .forEach((track) => localStream.addTrack(track));
480
+ * } catch (error) {
481
+ * console.error("Couldn't get camera permission:", error);
482
+ * }
483
+ * let oldTrackId;
484
+ * localStream
485
+ * .getTracks()
486
+ * .forEach((track) => trackId = webrtc.addTrack(track, localStream));
487
+ *
488
+ * // change camera
489
+ * const oldTrack = localStream.getVideoTracks()[0];
490
+ * let videoDeviceId = "abcd-1234";
491
+ * navigator.mediaDevices.getUserMedia({
492
+ * video: {
493
+ * ...(VIDEO_CONSTRAINTS as {}),
494
+ * deviceId: {
495
+ * exact: videoDeviceId,
496
+ * },
497
+ * }
498
+ * })
499
+ * .then((stream) => {
500
+ * let videoTrack = stream.getVideoTracks()[0];
501
+ * webrtc.replaceTrack(oldTrackId, videoTrack);
502
+ * })
503
+ * .catch((error) => {
504
+ * console.error('Error switching camera', error);
505
+ * })
506
+ * ```
507
+ *
508
+ * @param {string} trackId - Id of audio or video track to replace.
509
+ * @param {MediaStreamTrack} newTrack - New audio or video track.
510
+ * @returns {Promise<boolean>} Success
511
+ */
512
+ async replaceTrack(trackId, newTrack) {
513
+ if (!this.webrtc)
514
+ throw this.handleWebRTCNotInitialized();
515
+ return this.webrtc.replaceTrack(trackId, newTrack);
516
+ }
517
+ /**
518
+ * Updates maximum bandwidth for the track identified by trackId. This value directly translates to quality of the
519
+ * stream and, in case of video, to the amount of RTP packets being sent. In case trackId points at the simulcast
520
+ * track bandwidth is split between all of the variant streams proportionally to their resolution.
521
+ *
522
+ * @param {string} trackId
523
+ * @param {BandwidthLimit} bandwidth In kbps
524
+ * @returns {Promise<boolean>} Success
525
+ */
526
+ async setTrackBandwidth(trackId, bandwidth) {
527
+ if (!this.webrtc)
528
+ throw this.handleWebRTCNotInitialized();
529
+ await this.webrtc.setTrackBandwidth(trackId, bandwidth);
530
+ return true;
531
+ }
532
+ /**
533
+ * Updates maximum bandwidth for the given simulcast encoding of the given track.
534
+ *
535
+ * @param {string} trackId - Id of the track
536
+ * @param {string} rid - Rid of the encoding
537
+ * @param {BandwidthLimit} bandwidth - Desired max bandwidth used by the encoding (in kbps)
538
+ * @returns
539
+ */
540
+ async setEncodingBandwidth(trackId, rid, bandwidth) {
541
+ if (!this.webrtc)
542
+ throw this.handleWebRTCNotInitialized();
543
+ await this.webrtc.setEncodingBandwidth(trackId, rid, bandwidth);
544
+ return true;
545
+ }
546
+ /**
547
+ * Removes a track from connection that was being sent to the RTC Engine.
548
+ *
549
+ * @example
550
+ * ```ts
551
+ * // setup camera
552
+ * let localStream: MediaStream = new MediaStream();
553
+ * try {
554
+ * localVideoStream = await navigator.mediaDevices.getUserMedia(
555
+ * VIDEO_CONSTRAINTS
556
+ * );
557
+ * localVideoStream
558
+ * .getTracks()
559
+ * .forEach((track) => localStream.addTrack(track));
560
+ * } catch (error) {
561
+ * console.error("Couldn't get camera permission:", error);
562
+ * }
563
+ *
564
+ * let trackId
565
+ * localStream
566
+ * .getTracks()
567
+ * .forEach((track) => trackId = webrtc.addTrack(track, localStream));
568
+ *
569
+ * // remove track
570
+ * webrtc.removeTrack(trackId)
571
+ * ```
572
+ *
573
+ * @param {string} trackId - Id of audio or video track to remove.
574
+ */
575
+ removeTrack(trackId) {
576
+ if (!this.webrtc)
577
+ throw this.handleWebRTCNotInitialized();
578
+ return this.webrtc.removeTrack(trackId);
579
+ }
580
+ /**
581
+ * Sets track encoding that server should send to the client library.
582
+ *
583
+ * The encoding will be sent whenever it is available. If chosen encoding is temporarily unavailable, some other
584
+ * encoding will be sent until chosen encoding becomes active again.
585
+ *
586
+ * @example
587
+ * ```ts
588
+ * webrtc.setTargetTrackEncoding(incomingTrackCtx.trackId, "l")
589
+ * ```
590
+ *
591
+ * @param {string} trackId - Id of track
592
+ * @param {Encoding} encoding - Encoding to receive
593
+ */
594
+ setTargetTrackEncoding(trackId, encoding) {
595
+ if (!this.webrtc)
596
+ throw this.handleWebRTCNotInitialized();
597
+ return this.webrtc.setTargetTrackEncoding(trackId, encoding);
598
+ }
599
+ /**
600
+ * Enables track encoding so that it will be sent to the server.
601
+ *
602
+ * @example
603
+ * ```ts
604
+ * const trackId = webrtc.addTrack(track, stream, {}, {enabled: true, active_encodings: ["l", "m", "h"]});
605
+ * webrtc.disableTrackEncoding(trackId, "l");
606
+ * // wait some time
607
+ * webrtc.enableTrackEncoding(trackId, "l");
608
+ * ```
609
+ *
610
+ * @param {string} trackId - Id of track
611
+ * @param {Encoding} encoding - Encoding that will be enabled
612
+ */
613
+ enableTrackEncoding(trackId, encoding) {
614
+ if (!this.webrtc)
615
+ throw this.handleWebRTCNotInitialized();
616
+ return this.webrtc.enableTrackEncoding(trackId, encoding);
617
+ }
618
+ /**
619
+ * Disables track encoding so that it will be no longer sent to the server.
620
+ *
621
+ * @example
622
+ * ```ts
623
+ * const trackId = webrtc.addTrack(track, stream, {}, {enabled: true, active_encodings: ["l", "m", "h"]});
624
+ * webrtc.disableTrackEncoding(trackId, "l");
625
+ * ```
626
+ *
627
+ * @param {string} trackId - Id of track
628
+ * @param {Encoding} encoding - Encoding that will be disabled
629
+ */
630
+ disableTrackEncoding(trackId, encoding) {
631
+ if (!this.webrtc)
632
+ throw this.handleWebRTCNotInitialized();
633
+ return this.webrtc.disableTrackEncoding(trackId, encoding);
634
+ }
635
+ /**
636
+ * Updates the metadata for the current peer.
637
+ *
638
+ * @param peerMetadata - Data about this peer that other peers will receive upon joining.
639
+ *
640
+ * If the metadata is different from what is already tracked in the room, the event {@link MessageEvents.peerUpdated} will
641
+ * be emitted for other peers in the room.
642
+ */
643
+ updatePeerMetadata = (peerMetadata) => {
644
+ if (!this.webrtc)
645
+ throw this.handleWebRTCNotInitialized();
646
+ this.webrtc.updateEndpointMetadata(peerMetadata);
647
+ };
648
+ /**
649
+ * Updates the metadata for a specific track.
650
+ *
651
+ * @param trackId - TrackId (generated in addTrack) of audio or video track.
652
+ * @param trackMetadata - Data about this track that other peers will receive upon joining.
653
+ *
654
+ * If the metadata is different from what is already tracked in the room, the event {@link MessageEvents.trackUpdated} will
655
+ * be emitted for other peers in the room.
656
+ */
657
+ updateTrackMetadata = (trackId, trackMetadata) => {
658
+ if (!this.webrtc)
659
+ throw this.handleWebRTCNotInitialized();
660
+ this.webrtc.updateTrackMetadata(trackId, trackMetadata);
661
+ };
662
+ isReconnecting() {
663
+ return this.reconnectManager.isReconnecting();
664
+ }
665
+ getDataChannelsReadiness() {
666
+ return this.webrtc?.getDataChannelsReadiness() ?? false;
667
+ }
668
+ /**
669
+ * Leaves the room. This function should be called when user leaves the room in a clean way e.g. by clicking a
670
+ * dedicated, custom button `disconnect`. As a result there will be generated one more media event that should be sent
671
+ * to the RTC Engine. Thanks to it each other peer will be notified that peer left in {@link MessageEvents.peerLeft},
672
+ */
673
+ leave = () => {
674
+ if (!this.webrtc)
675
+ throw this.handleWebRTCNotInitialized();
676
+ this.webrtc.disconnect();
677
+ };
678
+ // https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState
679
+ isOpen(websocket) {
680
+ return websocket?.readyState === 1;
681
+ }
682
+ /**
683
+ * Create both reliable and lossy data channel publishers.
684
+ * This method must be called before publishData() can be used (unless negotiateOnConnect is enabled).
685
+ * Emits the 'dataChannelsReady' event when both channels are open and ready.
686
+ *
687
+ * @throws Error if data channels are not enabled in the constructor config
688
+ *
689
+ * @example
690
+ * ```typescript
691
+ * const client = new FishjamClient({ dataChannels: {} });
692
+ *
693
+ * client.on('dataChannelsReady', () => {
694
+ * console.log('Data channels ready, can now send data');
695
+ * client.publishData(new TextEncoder().encode('Hello'), { reliable: true });
696
+ * });
697
+ *
698
+ * client.createDataChannels();
699
+ * ```
700
+ */
701
+ createDataChannels() {
702
+ if (!this.webrtc)
703
+ throw this.handleWebRTCNotInitialized();
704
+ return this.webrtc.connectDataChannels();
705
+ }
706
+ /**
707
+ * Publish data through a data channel.
708
+ * The data channels must be created first by calling createDataChannels() or enabling negotiateOnConnect.
709
+ * Throws an error if the channel doesn't exist or isn't ready yet.
710
+ *
711
+ * @param data - The data to send as Uint8Array
712
+ * @param options - Options specifying which channel to use (reliable or lossy)
713
+ * @throws Error if the channel doesn't exist or isn't ready, or if webrtc is not initialized
714
+ *
715
+ * @example
716
+ * ```typescript
717
+ * client.on('dataChannelsReady', () => {
718
+ * // Send reliable data
719
+ * const data = new TextEncoder().encode('Hello World');
720
+ * client.publishData(data, { reliable: true });
721
+ *
722
+ * // Send lossy data for low-latency updates
723
+ * const gameState = new Uint8Array([1, 2, 3, 4, 5]);
724
+ * client.publishData(gameState, { reliable: false });
725
+ * });
726
+ *
727
+ * client.createDataChannels();
728
+ * ```
729
+ */
730
+ publishData(data, options) {
731
+ if (!this.webrtc)
732
+ throw this.handleWebRTCNotInitialized();
733
+ const message = ChannelMessage.encode({
734
+ source: 'mock',
735
+ destinations: ['*'],
736
+ binary: { data },
737
+ }).finish();
738
+ this.webrtc.publishData(message, options);
739
+ }
740
+ /**
741
+ * Subscribe to data from a specific channel type.
742
+ * Can be called before or after creating the data channels.
743
+ * If called before, the callback will be applied when the channel is created.
744
+ *
745
+ * @param callback - Function to call when data is received
746
+ * @param options - Options specifying which channel to subscribe to (reliable or lossy)
747
+ * @throws Error if webrtc is not initialized
748
+ *
749
+ * @example
750
+ * ```typescript
751
+ * // Subscribe to reliable channel
752
+ * client.subscribeData((data) => {
753
+ * const message = new TextDecoder().decode(data);
754
+ * console.log('Received:', message);
755
+ * }, { reliable: true });
756
+ *
757
+ * // Subscribe to lossy channel
758
+ * client.subscribeData((data) => {
759
+ * console.log('Received game state:', data);
760
+ * }, { reliable: false });
761
+ *
762
+ * // Then create publishers
763
+ * client.createDataChannels();
764
+ * ```
765
+ */
766
+ subscribeData(callback, options) {
767
+ if (!this.webrtc)
768
+ throw this.handleWebRTCNotInitialized();
769
+ const publisherCb = ({ channelType, data }) => {
770
+ if (options.reliable && channelType !== 'reliable')
771
+ return;
772
+ if (!options.reliable && channelType !== 'lossy')
773
+ return;
774
+ try {
775
+ const { binary } = ChannelMessage.decode(data);
776
+ if (binary) {
777
+ callback(binary.data);
778
+ }
779
+ }
780
+ catch (e) {
781
+ this.logger.warn(`Received invalid channel message, error: ${e}`);
782
+ }
783
+ };
784
+ this.webrtc.on('dataChannelPayload', publisherCb);
785
+ return () => this.webrtc?.off('dataChannelPayload', publisherCb);
786
+ }
787
+ /**
788
+ * Disconnect from the room, and close the websocket connection. Tries to leave the room gracefully, but if it fails,
789
+ * it will close the websocket anyway.
790
+ *
791
+ * @example
792
+ * ```typescript
793
+ * const client = new FishjamClient<PeerMetadata>();
794
+ *
795
+ * client.connect({ ... });
796
+ *
797
+ * client.disconnect();
798
+ * ```
799
+ */
800
+ disconnect() {
801
+ try {
802
+ this.webrtc?.removeAllListeners();
803
+ this.webrtc?.disconnect();
804
+ this.webrtc?.cleanUp();
805
+ }
806
+ catch (e) {
807
+ this.logger.warn(e);
808
+ }
809
+ this.removeEventListeners?.();
810
+ this.removeEventListeners = null;
811
+ if (this.isOpen(this.websocket || null)) {
812
+ this.websocket?.close();
813
+ }
814
+ this.websocket = null;
815
+ this.webrtc = null;
816
+ this.emit('disconnected');
817
+ }
818
+ cleanup() {
819
+ this.reconnectManager.cleanup();
820
+ }
821
+ }