@fishjam-cloud/ts-client 0.7.0 → 0.7.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.
package/README.md CHANGED
@@ -4,7 +4,12 @@
4
4
 
5
5
  # Fishjam TS Client
6
6
 
7
- TypeScript client library for [Fishjam Cloud](https://cloud.fishjam.stream).
7
+ TypeScript client library for [Fishjam Cloud](https://fishjam.io).
8
+
9
+ > [!WARNING]
10
+ > This SDK is not stable yet. We recommend to use
11
+ > [React Client](https://github.com/fishjam-cloud/web-client-sdk/tree/main/packages/react-client) for Fishjam Cloud
12
+ > services.
8
13
 
9
14
  ## Documentation
10
15
 
@@ -33,10 +38,11 @@ yarn @fishjam-cloud/ts-client
33
38
 
34
39
  Prerequisites:
35
40
 
36
- - Account on [Fishjam Cloud](https://cloud.fishjam.stream) with App configured.
41
+ - Account on [Fishjam Cloud](https://https://fishjam.io) with App configured.
37
42
  - Created room and token of peer in that room. You can use Room Manager to create room and peer token.
38
43
 
39
- The following code snippet is based on the [minimal](../../examples/ts-client/minimal/) example.
44
+ The following code snippet is based on the
45
+ [minimal](https://github.com/fishjam-cloud/web-client-sdk/tree/main/examples/ts-client/minimal/) example.
40
46
 
41
47
  ```ts
42
48
  import { FishjamClient, WebRTCEndpoint } from '@fishjam-cloud/ts-client';
@@ -119,12 +125,18 @@ async function startScreenSharing(webrtc: WebRTCEndpoint) {
119
125
 
120
126
  ## Examples
121
127
 
122
- For more examples, see the [examples](../../examples/ts-client/) folder.
128
+ For more examples, see the [examples](https://github.com/fishjam-cloud/web-client-sdk/tree/main/examples/ts-client/)
129
+ folder.
130
+
131
+ ## License
123
132
 
124
- ## Copyright and License
133
+ Licensed under the [Apache License, Version 2.0](LICENSE)
125
134
 
126
- Copyright 2024, [Software Mansion](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=fishjam-ts)
135
+ ## Fishjam Cloud is created by Software Mansion
127
136
 
128
- [![Software Mansion](https://logo.swmansion.com/logo?color=white&variant=desktop&width=200&tag=fishjam-github)](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=fishjam-ts)
137
+ Since 2012 [Software Mansion](https://swmansion.com) is a software agency with experience in building web and mobile
138
+ apps. We are Core React Native Contributors and experts in dealing with all kinds of React Native issues. We can help
139
+ you build your next dream product –
140
+ [Hire us](https://swmansion.com/contact/projects?utm_source=fishjam&utm_medium=web-readme).
129
141
 
130
- Licensed under the [Apache License, Version 2.0](LICENSE)
142
+ [![Software Mansion](https://logo.swmansion.com/logo?color=white&variant=desktop&width=200&tag=react-client)](https://swmansion.com/contact/projects?utm_source=fishjam&utm_medium=web-readme)
@@ -61,7 +61,7 @@ export class ReconnectManager {
61
61
  this.reconnectTimeoutId = null;
62
62
  }
63
63
  getLastPeerMetadata() {
64
- return this.lastLocalEndpoint?.metadata;
64
+ return this.lastLocalEndpoint?.metadata?.peer;
65
65
  }
66
66
  reconnect() {
67
67
  if (this.reconnectTimeoutId)
@@ -2,10 +2,10 @@ import type { Deferred } from './deferred';
2
2
  import type { LocalTrackManager } from './tracks/LocalTrackManager';
3
3
  import type { ConnectionManager } from './ConnectionManager';
4
4
  export type Command = {
5
- handler: () => void;
5
+ handler: () => Promise<void>;
6
6
  parse?: () => void;
7
7
  resolutionNotifier: Deferred<void>;
8
- resolve: 'after-renegotiation' | 'immediately';
8
+ resolve: 'after-renegotiation' | 'on-handler-resolve';
9
9
  };
10
10
  export declare class CommandsQueue<EndpointMetadata, TrackMetadata> {
11
11
  private readonly localTrackManager;
@@ -56,11 +56,12 @@ export class CommandsQueue {
56
56
  this.commandResolutionNotifier = command.resolutionNotifier;
57
57
  this.handleCommand(command);
58
58
  };
59
- handleCommand = (command) => {
59
+ handleCommand = async (command) => {
60
60
  try {
61
61
  command.parse?.();
62
- command.handler();
63
- if (command.resolve === 'immediately') {
62
+ const promise = command.handler();
63
+ if (command.resolve === 'on-handler-resolve') {
64
+ await promise;
64
65
  this.resolvePreviousCommand();
65
66
  this.processNextCommand();
66
67
  }
@@ -13,8 +13,14 @@ export class Local {
13
13
  localEndpoint = {
14
14
  id: '',
15
15
  type: 'webrtc',
16
- metadata: undefined,
17
- rawMetadata: undefined,
16
+ metadata: {
17
+ peer: undefined,
18
+ server: undefined,
19
+ },
20
+ rawMetadata: {
21
+ peer: undefined,
22
+ server: undefined,
23
+ },
18
24
  tracks: new Map(),
19
25
  };
20
26
  endpointMetadataParser;
@@ -96,15 +102,15 @@ export class Local {
96
102
  };
97
103
  setEndpointMetadata = (metadata) => {
98
104
  try {
99
- this.localEndpoint.metadata = this.endpointMetadataParser(metadata);
105
+ this.localEndpoint.metadata.peer = this.endpointMetadataParser(metadata);
100
106
  this.localEndpoint.metadataParsingError = undefined;
101
107
  }
102
108
  catch (error) {
103
- this.localEndpoint.metadata = undefined;
109
+ this.localEndpoint.metadata.peer = undefined;
104
110
  this.localEndpoint.metadataParsingError = error;
105
111
  throw error;
106
112
  }
107
- this.localEndpoint.rawMetadata = metadata;
113
+ this.localEndpoint.rawMetadata.peer = metadata;
108
114
  };
109
115
  getEndpoint = () => {
110
116
  return this.localEndpoint;
@@ -149,11 +155,11 @@ export class Local {
149
155
  });
150
156
  };
151
157
  updateEndpointMetadata = (metadata) => {
152
- this.localEndpoint.metadata = this.endpointMetadataParser(metadata);
158
+ this.localEndpoint.metadata.peer = this.endpointMetadataParser(metadata);
153
159
  this.localEndpoint.rawMetadata = this.localEndpoint.metadata;
154
160
  this.localEndpoint.metadataParsingError = undefined;
155
161
  const mediaEvent = generateMediaEvent('updateEndpointMetadata', {
156
- metadata: this.localEndpoint.metadata,
162
+ metadata: this.localEndpoint.metadata.peer,
157
163
  });
158
164
  this.sendMediaEvent(mediaEvent);
159
165
  this.emit('localEndpointMetadataChanged', {
@@ -1,7 +1,8 @@
1
- import { generateCustomEvent, generateMediaEvent } from '../mediaEvent';
1
+ import { generateCustomEvent } from '../mediaEvent';
2
2
  import { defaultBitrates, defaultSimulcastBitrates, UNLIMITED_BANDWIDTH } from '../bitrate';
3
3
  import { getEncodingParameters } from './encodings';
4
4
  import { createTransceiverConfig } from './transceivers';
5
+ import { emitMutableEvents, getActionType } from './muteTrackUtils';
5
6
  /**
6
7
  * This is a wrapper over `TrackContext` that adds additional properties such as:
7
8
  * - `MLineId`: required to generate sdpOffer
@@ -99,6 +100,7 @@ export class LocalTrack {
99
100
  replaceTrack = async (newTrack, webrtc) => {
100
101
  const trackId = this.id;
101
102
  const stream = this.trackContext.stream;
103
+ const oldTrack = this.trackContext.track;
102
104
  if (!this.sender)
103
105
  throw Error('There is no RTCRtpSender for this track id!');
104
106
  stream?.getTracks().forEach((track) => {
@@ -107,25 +109,25 @@ export class LocalTrack {
107
109
  if (newTrack) {
108
110
  stream?.addTrack(newTrack);
109
111
  }
110
- if (this.trackContext.track && !newTrack) {
111
- const mediaEvent = generateMediaEvent('muteTrack', { trackId: trackId });
112
- webrtc.sendMediaEvent(mediaEvent);
113
- webrtc.emit('localTrackMuted', { trackId: trackId });
114
- }
115
- else if (!this.trackContext.track && newTrack) {
116
- const mediaEvent = generateMediaEvent('unmuteTrack', {
117
- trackId: trackId,
118
- });
119
- webrtc.sendMediaEvent(mediaEvent);
120
- webrtc.emit('localTrackUnmuted', { trackId: trackId });
121
- }
112
+ this.trackContext.track = newTrack;
122
113
  this.mediaStreamTrackId = newTrack?.id ?? null;
114
+ const action = getActionType(this.trackContext.track, newTrack);
115
+ if (action === 'mute' || action === 'unmute') {
116
+ emitMutableEvents(action, webrtc, trackId);
117
+ }
123
118
  try {
124
119
  await this.sender.replaceTrack(newTrack);
125
- this.trackContext.track = newTrack;
126
120
  }
127
121
  catch (_error) {
128
- // ignore
122
+ // rollback: emit opposite events and revert internal state
123
+ if (action === 'mute') {
124
+ emitMutableEvents('unmute', webrtc, trackId);
125
+ }
126
+ else if (action === 'unmute') {
127
+ emitMutableEvents('mute', webrtc, trackId);
128
+ }
129
+ this.trackContext.track = oldTrack;
130
+ this.mediaStreamTrackId = oldTrack?.id ?? null;
129
131
  }
130
132
  };
131
133
  setTrackBandwidth = (bandwidth) => {
@@ -56,13 +56,13 @@ export class Remote {
56
56
  const newEndpoint = {
57
57
  id: endpoint.id,
58
58
  type: endpoint.type,
59
- metadata: undefined,
60
- rawMetadata: undefined,
59
+ metadata: { peer: undefined, server: undefined },
60
+ rawMetadata: { peer: undefined, server: undefined },
61
61
  metadataParsingError: undefined,
62
62
  tracks: new Map(),
63
63
  };
64
64
  // mutation in place
65
- this.updateEndpointMetadata(newEndpoint, endpoint.metadata);
65
+ this.updateEndpointMetadata(newEndpoint, endpoint?.metadata?.peer);
66
66
  this.addEndpoint(newEndpoint);
67
67
  this.addTracks(newEndpoint.id, endpoint.tracks, endpoint.trackIdToMetadata);
68
68
  if (sendNotification) {
@@ -77,19 +77,19 @@ export class Remote {
77
77
  if (!endpoint)
78
78
  throw new Error(`Endpoint ${data.id} not found`);
79
79
  // mutation in place
80
- this.updateEndpointMetadata(endpoint, data.metadata);
80
+ this.updateEndpointMetadata(endpoint, data.metadata.peer);
81
81
  this.emit('endpointUpdated', endpoint);
82
82
  };
83
83
  updateEndpointMetadata = (endpoint, metadata) => {
84
84
  try {
85
- endpoint.metadata = this.endpointMetadataParser(metadata);
85
+ endpoint.metadata.peer = this.endpointMetadataParser(metadata);
86
86
  endpoint.metadataParsingError = undefined;
87
87
  }
88
88
  catch (error) {
89
- endpoint.metadata = undefined;
89
+ endpoint.metadata.peer = undefined;
90
90
  endpoint.metadataParsingError = error;
91
91
  }
92
- endpoint.rawMetadata = metadata;
92
+ endpoint.rawMetadata.peer = metadata;
93
93
  };
94
94
  removeRemoteEndpoint = (endpointId) => {
95
95
  const endpoint = this.remoteEndpoints[endpointId];
@@ -0,0 +1,3 @@
1
+ import type { WebRTCEndpoint } from '../webRTCEndpoint';
2
+ export declare function emitMutableEvents<EndpointMetadata, TrackMetadata>(action: 'mute' | 'unmute', webrtc: WebRTCEndpoint<EndpointMetadata, TrackMetadata>, trackId: string): void;
3
+ export declare function getActionType(currentTrack: MediaStreamTrack | null, newTrack: MediaStreamTrack | null): 'mute' | 'unmute' | 'replace';
@@ -0,0 +1,19 @@
1
+ import { generateMediaEvent } from '../mediaEvent';
2
+ export function emitMutableEvents(action, webrtc, trackId) {
3
+ const mediaEventType = action === 'mute' ? `muteTrack` : `unmuteTrack`;
4
+ const localEventType = action === 'mute' ? `localTrackMuted` : `localTrackUnmuted`;
5
+ const mediaEvent = generateMediaEvent(mediaEventType, { trackId: trackId });
6
+ webrtc.sendMediaEvent(mediaEvent);
7
+ webrtc.emit(localEventType, { trackId: trackId });
8
+ }
9
+ export function getActionType(currentTrack, newTrack) {
10
+ if (currentTrack && !newTrack) {
11
+ return 'mute';
12
+ }
13
+ else if (!currentTrack && newTrack) {
14
+ return 'unmute';
15
+ }
16
+ else {
17
+ return 'replace';
18
+ }
19
+ }
@@ -268,7 +268,10 @@ export interface WebRTCEndpointEvents<EndpointMetadata, TrackMetadata> {
268
268
  encoding: Encoding;
269
269
  }) => void;
270
270
  localEndpointMetadataChanged: (event: {
271
- metadata: EndpointMetadata;
271
+ metadata: {
272
+ peer?: EndpointMetadata;
273
+ server?: unknown;
274
+ };
272
275
  }) => void;
273
276
  localTrackMetadataChanged: (event: {
274
277
  trackId: string;
@@ -294,8 +297,14 @@ export interface Endpoint<EndpointMetadata, TrackMetadata> {
294
297
  /**
295
298
  * Any information that was provided in {@link WebRTCEndpoint.connect}.
296
299
  */
297
- metadata?: EndpointMetadata;
298
- rawMetadata: any;
300
+ metadata: {
301
+ peer?: EndpointMetadata;
302
+ server: any;
303
+ };
304
+ rawMetadata: {
305
+ peer?: any;
306
+ server: any;
307
+ };
299
308
  metadataParsingError?: any;
300
309
  /**
301
310
  * List of tracks that are sent by the endpoint.
@@ -327,9 +327,7 @@ export class WebRTCEndpoint extends EventEmitter {
327
327
  metadata = parsedMetadata;
328
328
  stream.addTrack(track);
329
329
  this.commandsQueue.pushCommand({
330
- handler: () => {
331
- this.localTrackManager.addTrackHandler(trackId, track, stream, parsedMetadata, simulcastConfig, maxBandwidth);
332
- },
330
+ handler: async () => this.localTrackManager.addTrackHandler(trackId, track, stream, parsedMetadata, simulcastConfig, maxBandwidth),
333
331
  parse: () => this.localTrackManager.parseAddTrack(track, simulcastConfig, maxBandwidth),
334
332
  resolve: 'after-renegotiation',
335
333
  resolutionNotifier,
@@ -397,11 +395,9 @@ export class WebRTCEndpoint extends EventEmitter {
397
395
  async replaceTrack(trackId, newTrack) {
398
396
  const resolutionNotifier = new Deferred();
399
397
  this.commandsQueue.pushCommand({
400
- handler: () => {
401
- this.localTrackManager.replaceTrackHandler(this, trackId, newTrack);
402
- },
398
+ handler: () => this.localTrackManager.replaceTrackHandler(this, trackId, newTrack),
403
399
  resolutionNotifier,
404
- resolve: 'immediately',
400
+ resolve: 'on-handler-resolve',
405
401
  });
406
402
  return resolutionNotifier.promise.then(() => {
407
403
  this.emit('localTrackReplaced', { trackId, track: newTrack });
@@ -466,9 +462,7 @@ export class WebRTCEndpoint extends EventEmitter {
466
462
  async removeTrack(trackId) {
467
463
  const resolutionNotifier = new Deferred();
468
464
  this.commandsQueue.pushCommand({
469
- handler: () => {
470
- this.localTrackManager.removeTrackHandler(trackId);
471
- },
465
+ handler: async () => this.localTrackManager.removeTrackHandler(trackId),
472
466
  resolutionNotifier,
473
467
  resolve: 'after-renegotiation',
474
468
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fishjam-cloud/ts-client",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "description": "Typescript client library for Fishjam Cloud",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Fishjam Cloud Team",
@@ -51,22 +51,22 @@
51
51
  "uuid": "^10.0.0"
52
52
  },
53
53
  "devDependencies": {
54
- "@playwright/test": "^1.47.1",
54
+ "@playwright/test": "^1.47.2",
55
55
  "@types/events": "^3.0.3",
56
- "@types/node": "^22.5.5",
56
+ "@types/node": "^22.7.5",
57
57
  "@types/uuid": "^10.0.0",
58
- "@vitest/coverage-v8": "^2.1.1",
58
+ "@vitest/coverage-v8": "^2.1.2",
59
59
  "fake-mediastreamtrack": "^1.2.0",
60
60
  "husky": "^9.1.6",
61
61
  "lint-staged": "^15.2.10",
62
62
  "react": "^18.2.0",
63
- "ts-proto": "^2.2.0",
63
+ "ts-proto": "^2.2.3",
64
64
  "typed-emitter": "^2.1.0",
65
- "typedoc": "^0.26.7",
65
+ "typedoc": "^0.26.8",
66
66
  "typedoc-plugin-external-resolver": "^1.0.3",
67
- "typedoc-plugin-mdn-links": "^3.2.12",
67
+ "typedoc-plugin-mdn-links": "^3.3.2",
68
68
  "typescript": "^5.6.2",
69
- "vitest": "^2.1.1",
69
+ "vitest": "^2.1.2",
70
70
  "zod": "^3.23.6"
71
71
  },
72
72
  "lint-staged": {