@stream-io/video-client 0.0.28 → 0.0.29

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 (67) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/index.browser.es.js +2514 -1757
  3. package/dist/index.browser.es.js.map +1 -1
  4. package/dist/index.cjs.js +2534 -1755
  5. package/dist/index.cjs.js.map +1 -1
  6. package/dist/index.es.js +2514 -1757
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/src/Call.d.ts +2 -3
  9. package/dist/src/StreamSfuClient.d.ts +23 -10
  10. package/dist/src/StreamVideoClient.d.ts +1 -4
  11. package/dist/src/client-details.d.ts +2 -1
  12. package/dist/src/coordinator/connection/types.d.ts +2 -2
  13. package/dist/src/coordinator/connection/utils.d.ts +1 -0
  14. package/dist/src/events/internal.d.ts +4 -0
  15. package/dist/src/gen/coordinator/index.d.ts +6 -0
  16. package/dist/src/gen/google/protobuf/struct.d.ts +8 -15
  17. package/dist/src/gen/google/protobuf/timestamp.d.ts +2 -9
  18. package/dist/src/gen/video/sfu/event/events.d.ts +121 -1
  19. package/dist/src/gen/video/sfu/models/models.d.ts +38 -1
  20. package/dist/src/gen/video/sfu/signal_rpc/signal.client.d.ts +3 -14
  21. package/dist/src/gen/video/sfu/signal_rpc/signal.d.ts +4 -12
  22. package/dist/src/logger.d.ts +4 -2
  23. package/dist/src/rtc/Dispatcher.d.ts +1 -2
  24. package/dist/src/rtc/{publisher.d.ts → Publisher.d.ts} +49 -15
  25. package/dist/src/rtc/Subscriber.d.ts +58 -0
  26. package/dist/src/rtc/__tests__/Subscriber.test.d.ts +1 -0
  27. package/dist/src/rtc/flows/join.d.ts +8 -1
  28. package/dist/src/rtc/index.d.ts +2 -2
  29. package/dist/src/rtc/signal.d.ts +1 -0
  30. package/dist/src/stats/state-store-stats-reporter.d.ts +3 -4
  31. package/dist/src/store/CallState.d.ts +10 -0
  32. package/package.json +3 -1
  33. package/src/Call.ts +215 -209
  34. package/src/StreamSfuClient.ts +48 -21
  35. package/src/StreamVideoClient.ts +7 -24
  36. package/src/client-details.ts +33 -1
  37. package/src/coordinator/connection/client.ts +6 -8
  38. package/src/coordinator/connection/types.ts +2 -3
  39. package/src/coordinator/connection/utils.ts +1 -0
  40. package/src/events/call.ts +0 -1
  41. package/src/events/callEventHandlers.ts +2 -0
  42. package/src/events/internal.ts +20 -0
  43. package/src/events/sessions.ts +0 -1
  44. package/src/gen/coordinator/index.ts +6 -0
  45. package/src/gen/google/protobuf/struct.ts +541 -333
  46. package/src/gen/google/protobuf/timestamp.ts +214 -148
  47. package/src/gen/video/sfu/event/events.ts +353 -3
  48. package/src/gen/video/sfu/models/models.ts +37 -0
  49. package/src/gen/video/sfu/signal_rpc/signal.client.ts +160 -94
  50. package/src/gen/video/sfu/signal_rpc/signal.ts +1214 -731
  51. package/src/logger.ts +43 -30
  52. package/src/rtc/Dispatcher.ts +5 -9
  53. package/src/rtc/{publisher.ts → Publisher.ts} +245 -111
  54. package/src/rtc/Subscriber.ts +304 -0
  55. package/src/rtc/__tests__/{publisher.test.ts → Publisher.test.ts} +77 -9
  56. package/src/rtc/__tests__/Subscriber.test.ts +121 -0
  57. package/src/rtc/__tests__/mocks/webrtc.mocks.ts +20 -0
  58. package/src/rtc/flows/join.ts +43 -2
  59. package/src/rtc/index.ts +2 -2
  60. package/src/rtc/signal.ts +6 -5
  61. package/src/rtc/videoLayers.ts +1 -4
  62. package/src/stats/state-store-stats-reporter.ts +3 -5
  63. package/src/store/CallState.ts +20 -0
  64. package/src/types.ts +0 -1
  65. package/dist/src/rtc/subscriber.d.ts +0 -9
  66. package/src/rtc/subscriber.ts +0 -107
  67. /package/dist/src/rtc/__tests__/{publisher.test.d.ts → Publisher.test.d.ts} +0 -0
@@ -1,3 +1,4 @@
1
+ import type { WebSocket } from 'ws';
1
2
  import type {
2
3
  FinishedUnaryCall,
3
4
  MethodInfo,
@@ -30,6 +31,7 @@ import {
30
31
  retryInterval,
31
32
  sleep,
32
33
  } from './coordinator/connection/utils';
34
+ import { SFUResponse } from './gen/coordinator';
33
35
  import { Logger } from './coordinator/connection/types';
34
36
  import { getLogger } from './logger';
35
37
 
@@ -40,14 +42,9 @@ export type StreamSfuClientConstructor = {
40
42
  dispatcher: Dispatcher;
41
43
 
42
44
  /**
43
- * The URL of the SFU to connect to.
45
+ * The SFU server to connect to.
44
46
  */
45
- url: string;
46
-
47
- /**
48
- * The WebSocket endpoint of the SFU to connect to.
49
- */
50
- wsEndpoint: string;
47
+ sfuServer: SFUResponse;
51
48
 
52
49
  /**
53
50
  * The JWT token to use for authentication.
@@ -75,6 +72,21 @@ export class StreamSfuClient {
75
72
  */
76
73
  readonly sessionId: string;
77
74
 
75
+ /**
76
+ * The `edgeName` representing the edge the client is connected to.
77
+ */
78
+ readonly edgeName: string;
79
+
80
+ /**
81
+ * The current token used for authenticating against the SFU.
82
+ */
83
+ readonly token: string;
84
+
85
+ /**
86
+ * The SFU server details the current client is connected to.
87
+ */
88
+ readonly sfuServer: SFUResponse;
89
+
78
90
  /**
79
91
  * Holds the current WebSocket connection to the SFU.
80
92
  */
@@ -85,8 +97,13 @@ export class StreamSfuClient {
85
97
  */
86
98
  signalReady: Promise<WebSocket>;
87
99
 
100
+ /**
101
+ * A flag indicating whether the client is currently migrating away
102
+ * from this SFU.
103
+ */
104
+ isMigratingAway = false;
105
+
88
106
  private readonly rpc: SignalServerClient;
89
- private readonly token: string;
90
107
  private keepAliveInterval?: NodeJS.Timeout;
91
108
  private connectionCheckTimeout?: NodeJS.Timeout;
92
109
  private pingIntervalInMs = 25 * 1000;
@@ -99,19 +116,19 @@ export class StreamSfuClient {
99
116
  * Constructs a new SFU client.
100
117
  *
101
118
  * @param dispatcher the event dispatcher to use.
102
- * @param url the URL of the SFU.
103
- * @param wsEndpoint the WebSocket endpoint of the SFU.
119
+ * @param sfuServer the SFU server to connect to.
104
120
  * @param token the JWT token to use for authentication.
105
121
  * @param sessionId the `sessionId` of the currently connected participant.
106
122
  */
107
123
  constructor({
108
124
  dispatcher,
109
- url,
110
- wsEndpoint,
125
+ sfuServer,
111
126
  token,
112
127
  sessionId,
113
128
  }: StreamSfuClientConstructor) {
114
129
  this.sessionId = sessionId || generateUUIDv4();
130
+ this.sfuServer = sfuServer;
131
+ this.edgeName = sfuServer.edge_name;
115
132
  this.token = token;
116
133
  this.logger = getLogger(['sfu-client']);
117
134
  const logger = this.logger;
@@ -122,13 +139,15 @@ export class StreamSfuClient {
122
139
  input: object,
123
140
  options: RpcOptions,
124
141
  ): UnaryCall {
125
- logger('info', `Calling SFU RPC method ${method.name}`);
126
- logger('debug', `Method call payload`, { input, options });
142
+ logger('trace', `Calling SFU RPC method ${method.name}`, {
143
+ input,
144
+ options,
145
+ });
127
146
  return next(method, input, options);
128
147
  },
129
148
  };
130
149
  this.rpc = createSignalClient({
131
- baseUrl: url,
150
+ baseUrl: sfuServer.url,
132
151
  interceptors: [
133
152
  withHeaders({
134
153
  Authorization: `Bearer ${token}`,
@@ -149,7 +168,7 @@ export class StreamSfuClient {
149
168
  });
150
169
 
151
170
  this.signalWs = createWebSocketSignalChannel({
152
- endpoint: wsEndpoint,
171
+ endpoint: sfuServer.ws_endpoint,
153
172
  onMessage: (message) => {
154
173
  this.lastMessageTimestamp = new Date();
155
174
  this.scheduleConnectionCheck();
@@ -158,10 +177,12 @@ export class StreamSfuClient {
158
177
  });
159
178
 
160
179
  this.signalReady = new Promise((resolve) => {
161
- this.signalWs.addEventListener('open', () => {
180
+ const onOpen = () => {
181
+ this.signalWs.removeEventListener('open', onOpen);
162
182
  this.keepAlive();
163
183
  resolve(this.signalWs);
164
- });
184
+ };
185
+ this.signalWs.addEventListener('open', onOpen);
165
186
  });
166
187
  }
167
188
 
@@ -262,6 +283,11 @@ export class StreamSfuClient {
262
283
 
263
284
  send = (message: SfuRequest) => {
264
285
  return this.signalReady.then((signal) => {
286
+ this.logger(
287
+ 'debug',
288
+ `Sending message to: ${this.edgeName}`,
289
+ SfuRequest.toJson(message),
290
+ );
265
291
  signal.send(SfuRequest.toBinary(message));
266
292
  });
267
293
  };
@@ -271,7 +297,7 @@ export class StreamSfuClient {
271
297
  clearInterval(this.keepAliveInterval);
272
298
  }
273
299
  this.keepAliveInterval = setInterval(() => {
274
- this.logger('info', 'Sending healthCheckRequest to SFU');
300
+ this.logger('trace', 'Sending healthCheckRequest to SFU');
275
301
  const message = SfuRequest.create({
276
302
  requestPayload: {
277
303
  oneofKind: 'healthCheckRequest',
@@ -326,6 +352,7 @@ const MAX_RETRIES = 5;
326
352
  * request bursts towards the SFU.
327
353
  *
328
354
  * @param rpc the closure around the RPC call to execute.
355
+ * @param logger a logger instance to use.
329
356
  * @param <I> the type of the request object.
330
357
  * @param <O> the type of the response object.
331
358
  */
@@ -343,10 +370,10 @@ const retryable = async <I extends object, O extends SfuResponseWithError>(
343
370
 
344
371
  rpcCallResult = await rpc();
345
372
  logger(
346
- 'info',
373
+ 'trace',
347
374
  `SFU RPC response received for ${rpcCallResult.method.name}`,
375
+ rpcCallResult,
348
376
  );
349
- logger('debug', `Response payload`, rpcCallResult);
350
377
 
351
378
  // if the RPC call failed, log the error and retry
352
379
  if (rpcCallResult.response.error) {
@@ -24,8 +24,8 @@ import type {
24
24
  ConnectionChangedEvent,
25
25
  EventHandler,
26
26
  EventTypes,
27
- LogLevel,
28
27
  Logger,
28
+ LogLevel,
29
29
  StreamClientOptions,
30
30
  TokenOrProvider,
31
31
  TokenProvider,
@@ -44,7 +44,6 @@ export class StreamVideoClient {
44
44
  readonly readOnlyStateStore: StreamVideoReadOnlyStateStore;
45
45
  readonly user?: User;
46
46
  readonly token?: TokenOrProvider;
47
- readonly logLevel: LogLevel = 'warn';
48
47
  readonly logger: Logger;
49
48
 
50
49
  private readonly writeableStateStore: StreamVideoWriteableStateStore;
@@ -53,7 +52,6 @@ export class StreamVideoClient {
53
52
  private eventHandlersToUnregister: Array<() => void> = [];
54
53
  private connectionPromise: Promise<void | ConnectedEvent> | undefined;
55
54
  private disconnectionPromise: Promise<void> | undefined;
56
- private logLevels: LogLevel[] = ['debug', 'info', 'warn', 'error'];
57
55
 
58
56
  /**
59
57
  * You should create only one instance of `StreamVideoClient`.
@@ -79,29 +77,30 @@ export class StreamVideoClient {
79
77
  opts?: StreamClientOptions,
80
78
  ) {
81
79
  let logger: Logger = logToConsole;
80
+ let logLevel: LogLevel = 'warn';
82
81
  if (typeof apiKeyOrArgs === 'string') {
83
- this.logLevel = opts?.logLevel || this.logLevel;
82
+ logLevel = opts?.logLevel || logLevel;
84
83
  logger = opts?.logger || logger;
85
84
  } else {
86
- this.logLevel = apiKeyOrArgs.options?.logLevel || this.logLevel;
85
+ logLevel = apiKeyOrArgs.options?.logLevel || logLevel;
87
86
  logger = apiKeyOrArgs.options?.logger || logger;
88
87
  }
89
88
 
90
- setLogger(this.filterLogs(logger));
89
+ setLogger(logger, logLevel);
91
90
  this.logger = getLogger(['client']);
92
91
 
93
92
  if (typeof apiKeyOrArgs === 'string') {
94
93
  this.streamClient = new StreamClient(apiKeyOrArgs, {
95
94
  persistUserOnConnectionFailure: true,
96
95
  ...opts,
97
- logLevel: this.logLevel,
96
+ logLevel,
98
97
  logger: this.logger,
99
98
  });
100
99
  } else {
101
100
  this.streamClient = new StreamClient(apiKeyOrArgs.apiKey, {
102
101
  persistUserOnConnectionFailure: true,
103
102
  ...apiKeyOrArgs.options,
104
- logLevel: this.logLevel,
103
+ logLevel,
105
104
  logger: this.logger,
106
105
  });
107
106
 
@@ -504,20 +503,4 @@ export class StreamVideoClient {
504
503
  this.connectionPromise.finally(() => (this.connectionPromise = undefined));
505
504
  return this.connectionPromise;
506
505
  };
507
-
508
- private filterLogs = (logMethod: Logger) => {
509
- return (
510
- logLevel: LogLevel,
511
- messeage: string,
512
- extraData?: Record<string, unknown>,
513
- tags?: string[],
514
- ) => {
515
- if (
516
- this.logLevels.indexOf(logLevel) >=
517
- this.logLevels.indexOf(this.logLevel)
518
- ) {
519
- logMethod(logLevel, messeage, extraData, tags);
520
- }
521
- };
522
- };
523
506
  }
@@ -1,4 +1,6 @@
1
- import { Device, OS, Sdk } from './gen/video/sfu/models/models';
1
+ import { ClientDetails, Device, OS, Sdk } from './gen/video/sfu/models/models';
2
+ import { isReactNative } from './helpers/platforms';
3
+ import { UAParser } from 'ua-parser-js';
2
4
 
3
5
  let sdkInfo: Sdk | undefined;
4
6
  let osInfo: OS | undefined;
@@ -27,3 +29,33 @@ export const setDeviceInfo = (info: Device) => {
27
29
  export const getDeviceInfo = () => {
28
30
  return deviceInfo;
29
31
  };
32
+
33
+ export const getClientDetails = (): ClientDetails => {
34
+ if (isReactNative()) {
35
+ // Since RN doesn't support web, sharing browser info is not required
36
+ return {
37
+ sdk: getSdkInfo(),
38
+ os: getOSInfo(),
39
+ device: getDeviceInfo(),
40
+ };
41
+ }
42
+
43
+ const userAgent = new UAParser(navigator.userAgent);
44
+ const { browser, os, device, cpu } = userAgent.getResult();
45
+ return {
46
+ sdk: getSdkInfo(),
47
+ browser: {
48
+ name: browser.name || navigator.userAgent,
49
+ version: browser.version || '',
50
+ },
51
+ os: {
52
+ name: os.name || '',
53
+ version: os.version || '',
54
+ architecture: cpu.architecture || '',
55
+ },
56
+ device: {
57
+ name: `${device.vendor || ''} ${device.model || ''} ${device.type || ''}`,
58
+ version: '',
59
+ },
60
+ };
61
+ };
@@ -134,8 +134,8 @@ export class StreamClient {
134
134
  }
135
135
 
136
136
  this.axiosInstance = axios.create({
137
- baseURL: this.baseURL,
138
137
  ...this.options,
138
+ baseURL: this.baseURL,
139
139
  });
140
140
 
141
141
  // WS connection is initialized when setUser is called
@@ -457,7 +457,7 @@ export class StreamClient {
457
457
  this.listeners[key] = [];
458
458
  }
459
459
 
460
- this.logger('info', `Removing listener for ${key} event`);
460
+ this.logger('debug', `Removing listener for ${key} event`);
461
461
  this.listeners[key] = this.listeners[key].filter(
462
462
  (value) => value !== callback,
463
463
  );
@@ -471,8 +471,7 @@ export class StreamClient {
471
471
  config?: AxiosRequestConfig & { maxBodyLength?: number };
472
472
  },
473
473
  ) {
474
- this.logger('info', `client: ${type} - Request - ${url}`);
475
- this.logger('debug', `client: ${type} - Request payload`, {
474
+ this.logger('trace', `client: ${type} - Request - ${url}`, {
476
475
  payload: data,
477
476
  config,
478
477
  });
@@ -480,13 +479,13 @@ export class StreamClient {
480
479
 
481
480
  _logApiResponse<T>(type: string, url: string, response: AxiosResponse<T>) {
482
481
  this.logger(
483
- 'info',
482
+ 'trace',
484
483
  `client:${type} - Response - url: ${url} > status ${response.status}`,
485
484
  {
486
485
  response,
487
486
  },
488
487
  );
489
- this.logger('debug', `client:${type} - Response payload`, {
488
+ this.logger('trace', `client:${type} - Response payload`, {
490
489
  response,
491
490
  });
492
491
  }
@@ -617,8 +616,7 @@ export class StreamClient {
617
616
  dispatchEvent = (event: StreamVideoEvent) => {
618
617
  if (!event.received_at) event.received_at = new Date();
619
618
 
620
- this.logger('info', `Dispatching event: ${event.type}`);
621
- this.logger('debug', 'Event payload:', event);
619
+ this.logger('debug', `Dispatching event: ${event.type}`, event);
622
620
  this._callClientListeners(event);
623
621
  };
624
622
 
@@ -24,7 +24,7 @@ export type { OwnUserResponse } from '../../gen/coordinator';
24
24
 
25
25
  export type ConnectAPIResponse = Promise<void | ConnectedEvent>;
26
26
 
27
- export type LogLevel = 'debug' | 'info' | 'error' | 'warn';
27
+ export type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error';
28
28
 
29
29
  type ErrorResponseDetails = {
30
30
  code: number;
@@ -81,8 +81,7 @@ export type CallEventTypes = StreamCallEvent['type'];
81
81
  export type Logger = (
82
82
  logLevel: LogLevel,
83
83
  message: string,
84
- extraData?: any,
85
- tags?: string[],
84
+ ...args: unknown[]
86
85
  ) => void;
87
86
 
88
87
  export type StreamClientOptions = Partial<AxiosRequestConfig> & {
@@ -18,6 +18,7 @@ export function isFunction<T>(value: Function | T): value is Function {
18
18
  export const KnownCodes = {
19
19
  TOKEN_EXPIRED: 40,
20
20
  WS_CLOSED_SUCCESS: 1000,
21
+ WS_CLOSED_ABRUPTLY: 1006,
21
22
  WS_POLICY_VIOLATION: 1008,
22
23
  };
23
24
 
@@ -1,7 +1,6 @@
1
1
  import { CallingState, CallState } from '../store';
2
2
  import { StreamVideoEvent } from '../coordinator/connection/types';
3
3
  import { Call } from '../Call';
4
- import { getLogger } from '../logger';
5
4
 
6
5
  /**
7
6
  * Event handler that watched the delivery of `call.accepted`.
@@ -31,6 +31,7 @@ import {
31
31
  watchParticipantCountChanged,
32
32
  watchParticipantJoined,
33
33
  watchParticipantLeft,
34
+ watchSfuErrorReports,
34
35
  watchTrackPublished,
35
36
  watchTrackUnpublished,
36
37
  watchUnblockedUser,
@@ -92,6 +93,7 @@ export const registerEventHandlers = (
92
93
  'call.user_muted': () => console.log('call.user_muted received'),
93
94
  };
94
95
  const eventHandlers = [
96
+ watchSfuErrorReports(dispatcher),
95
97
  watchChangePublishQuality(dispatcher, call),
96
98
  watchConnectionQualityChanged(dispatcher, state),
97
99
  watchParticipantCountChanged(dispatcher, state),
@@ -2,6 +2,10 @@ import { Dispatcher } from '../rtc';
2
2
  import { Call } from '../Call';
3
3
  import { CallState } from '../store';
4
4
  import { StreamVideoParticipantPatches } from '../types';
5
+ import { getLogger } from '../logger';
6
+ import { ErrorCode } from '../gen/video/sfu/models/models';
7
+
8
+ const logger = getLogger(['events']);
5
9
 
6
10
  /**
7
11
  * An event responder which handles the `changePublishQuality` event.
@@ -63,3 +67,19 @@ export const watchParticipantCountChanged = (
63
67
  }
64
68
  });
65
69
  };
70
+
71
+ /**
72
+ * Watches and logs the errors reported by the currently connected SFU.
73
+ */
74
+ export const watchSfuErrorReports = (dispatcher: Dispatcher) => {
75
+ return dispatcher.on('error', (e) => {
76
+ if (e.eventPayload.oneofKind !== 'error' || !e.eventPayload.error.error)
77
+ return;
78
+ const error = e.eventPayload.error.error;
79
+ logger('error', 'SFU reported error', {
80
+ code: ErrorCode[error.code],
81
+ message: error.message,
82
+ shouldRetry: error.shouldRetry,
83
+ });
84
+ });
85
+ };
@@ -1,6 +1,5 @@
1
1
  import { CallState } from '../store';
2
2
  import { StreamVideoEvent } from '../coordinator/connection/types';
3
- import { getLogger } from '../logger';
4
3
 
5
4
  /**
6
5
  * Watch for call.session_started events and update the call metadata.
@@ -2457,6 +2457,12 @@ export interface JoinCallRequest {
2457
2457
  * @memberof JoinCallRequest
2458
2458
  */
2459
2459
  members_limit?: number;
2460
+ /**
2461
+ * If the participant is migrating from another SFU, then this is the ID of the previous SFU
2462
+ * @type {string}
2463
+ * @memberof JoinCallRequest
2464
+ */
2465
+ migrating_from?: string;
2460
2466
  /**
2461
2467
  *
2462
2468
  * @type {boolean}