@stream-io/video-client 1.11.5 → 1.11.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.
Files changed (34) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/index.browser.es.js +77 -569
  3. package/dist/index.browser.es.js.map +1 -1
  4. package/dist/index.cjs.js +62 -554
  5. package/dist/index.cjs.js.map +1 -1
  6. package/dist/index.es.js +77 -569
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/src/coordinator/connection/client.d.ts +0 -18
  9. package/dist/src/coordinator/connection/connection.d.ts +4 -12
  10. package/dist/src/coordinator/connection/signing.d.ts +1 -7
  11. package/dist/src/coordinator/connection/token_manager.d.ts +0 -2
  12. package/dist/src/coordinator/connection/types.d.ts +5 -6
  13. package/dist/src/coordinator/connection/utils.d.ts +6 -8
  14. package/dist/src/rtc/codecs.d.ts +2 -1
  15. package/package.json +6 -10
  16. package/src/__tests__/Call.test.ts +3 -2
  17. package/src/coordinator/connection/client.ts +12 -149
  18. package/src/coordinator/connection/connection.ts +40 -109
  19. package/src/coordinator/connection/signing.ts +31 -17
  20. package/src/coordinator/connection/token_manager.ts +3 -9
  21. package/src/coordinator/connection/types.ts +5 -9
  22. package/src/coordinator/connection/utils.ts +18 -50
  23. package/src/devices/__tests__/InputMediaDeviceManagerState.test.ts +13 -8
  24. package/src/devices/__tests__/mocks.ts +0 -4
  25. package/src/rtc/Publisher.ts +8 -3
  26. package/src/rtc/codecs.ts +7 -3
  27. package/dist/src/coordinator/connection/base64.d.ts +0 -2
  28. package/dist/src/coordinator/connection/connection_fallback.d.ts +0 -39
  29. package/dist/src/coordinator/connection/errors.d.ts +0 -16
  30. package/dist/src/coordinator/connection/insights.d.ts +0 -57
  31. package/src/coordinator/connection/base64.ts +0 -80
  32. package/src/coordinator/connection/connection_fallback.ts +0 -242
  33. package/src/coordinator/connection/errors.ts +0 -80
  34. package/src/coordinator/connection/insights.ts +0 -88
@@ -1,39 +0,0 @@
1
- import { AxiosRequestConfig, CancelTokenSource } from 'axios';
2
- import { StreamClient } from './client';
3
- import { LogLevel, UR } from './types';
4
- import { ConnectedEvent } from '../../gen/coordinator';
5
- export declare enum ConnectionState {
6
- Closed = "CLOSED",
7
- Connected = "CONNECTED",
8
- Connecting = "CONNECTING",
9
- Disconnected = "DISCONNECTED",
10
- Init = "INIT"
11
- }
12
- export declare class WSConnectionFallback {
13
- client: StreamClient;
14
- state: ConnectionState;
15
- consecutiveFailures: number;
16
- connectionID?: string;
17
- cancelToken?: CancelTokenSource;
18
- constructor(client: StreamClient);
19
- _log(msg: string, extra?: UR, level?: LogLevel): void;
20
- _setState(state: ConnectionState): void;
21
- /** @private */
22
- _onlineStatusChanged: (event: {
23
- type: string;
24
- }) => void;
25
- /** @private */
26
- _req: <T = UR>(params: UR, config: AxiosRequestConfig, retry: boolean) => Promise<T>;
27
- /** @private */
28
- _poll: () => Promise<void>;
29
- /**
30
- * connect try to open a longpoll request
31
- * @param reconnect should be false for first call and true for subsequent calls to keep the connection alive and call recoverState
32
- */
33
- connect: (reconnect?: boolean) => Promise<ConnectedEvent | undefined>;
34
- /**
35
- * isHealthy checks if there is a connectionID and connection is in Connected state
36
- */
37
- isHealthy: () => boolean;
38
- disconnect: (timeout?: number) => Promise<void>;
39
- }
@@ -1,16 +0,0 @@
1
- import { AxiosResponse } from 'axios';
2
- import { APIErrorResponse } from './types';
3
- export declare const APIErrorCodes: Record<string, {
4
- name: string;
5
- retryable: boolean;
6
- }>;
7
- type APIError = Error & {
8
- code: number;
9
- isWSFailure?: boolean;
10
- };
11
- export declare function isAPIError(error: Error): error is APIError;
12
- export declare function isErrorRetryable(error: APIError): boolean;
13
- export declare function isConnectionIDError(error: APIError): boolean;
14
- export declare function isWSFailure(err: APIError): boolean;
15
- export declare function isErrorResponse(res: AxiosResponse<unknown>): res is AxiosResponse<APIErrorResponse>;
16
- export {};
@@ -1,57 +0,0 @@
1
- import { StableWSConnection } from './connection';
2
- export type InsightTypes = 'ws_fatal' | 'ws_success_after_failure' | 'http_hi_failed';
3
- export declare class InsightMetrics {
4
- connectionStartTimestamp: number | null;
5
- wsConsecutiveFailures: number;
6
- wsTotalFailures: number;
7
- instanceClientId: string;
8
- constructor();
9
- }
10
- /**
11
- * postInsights is not supposed to be used by end users directly within chat application, and thus is kept isolated
12
- * from all the client/connection code/logic.
13
- *
14
- * @param insightType
15
- * @param insights
16
- */
17
- export declare const postInsights: (insightType: InsightTypes, insights: Record<string, unknown>) => Promise<void>;
18
- export declare function buildWsFatalInsight(connection: StableWSConnection, event: Record<string, unknown>): {
19
- ready_state: 0 | 1 | 2 | 3 | undefined;
20
- url: string;
21
- api_key: string;
22
- start_ts: number | null;
23
- end_ts: number;
24
- auth_type: string;
25
- token: string | undefined;
26
- user_id: string | undefined;
27
- user_details: import("./types").UserWithId | undefined;
28
- device: string;
29
- client_id: string | undefined;
30
- ws_details: import("ws") | undefined;
31
- ws_consecutive_failures: number;
32
- ws_total_failures: number;
33
- request_id: string | undefined;
34
- online: boolean | null;
35
- user_agent: string | null;
36
- instance_client_id: string;
37
- };
38
- export declare function buildWsSuccessAfterFailureInsight(connection: StableWSConnection): {
39
- ready_state: 0 | 1 | 2 | 3 | undefined;
40
- url: string;
41
- api_key: string;
42
- start_ts: number | null;
43
- end_ts: number;
44
- auth_type: string;
45
- token: string | undefined;
46
- user_id: string | undefined;
47
- user_details: import("./types").UserWithId | undefined;
48
- device: string;
49
- client_id: string | undefined;
50
- ws_details: import("ws") | undefined;
51
- ws_consecutive_failures: number;
52
- ws_total_failures: number;
53
- request_id: string | undefined;
54
- online: boolean | null;
55
- user_agent: string | null;
56
- instance_client_id: string;
57
- };
@@ -1,80 +0,0 @@
1
- import { fromByteArray } from 'base64-js';
2
-
3
- function isString<T>(arrayOrString: string | T[]): arrayOrString is string {
4
- return typeof (arrayOrString as string) === 'string';
5
- }
6
-
7
- type MapGenericCallback<T, U> = (value: T, index: number, array: T[]) => U;
8
- type MapStringCallback<U> = (value: string, index: number, string: string) => U;
9
-
10
- function isMapStringCallback<T, U>(
11
- arrayOrString: string | T[],
12
- callback: MapGenericCallback<T, U> | MapStringCallback<U>,
13
- ): callback is MapStringCallback<U> {
14
- return !!callback && isString(arrayOrString);
15
- }
16
-
17
- // source - https://github.com/beatgammit/base64-js/blob/master/test/convert.js#L72
18
- function map<T, U>(array: T[], callback: MapGenericCallback<T, U>): U[];
19
- function map<U>(string: string, callback: MapStringCallback<U>): U[];
20
- function map<T, U>(
21
- arrayOrString: string | T[],
22
- callback: MapGenericCallback<T, U> | MapStringCallback<U>,
23
- ): U[] {
24
- const res = [];
25
-
26
- if (isString(arrayOrString) && isMapStringCallback(arrayOrString, callback)) {
27
- for (let k = 0, len = arrayOrString.length; k < len; k++) {
28
- if (arrayOrString.charAt(k)) {
29
- const kValue = arrayOrString.charAt(k);
30
- const mappedValue = callback(kValue, k, arrayOrString);
31
- res[k] = mappedValue;
32
- }
33
- }
34
- } else if (
35
- !isString(arrayOrString) &&
36
- !isMapStringCallback(arrayOrString, callback)
37
- ) {
38
- for (let k = 0, len = arrayOrString.length; k < len; k++) {
39
- if (k in arrayOrString) {
40
- const kValue = arrayOrString[k];
41
- const mappedValue = callback(kValue, k, arrayOrString);
42
- res[k] = mappedValue;
43
- }
44
- }
45
- }
46
-
47
- return res;
48
- }
49
-
50
- export const encodeBase64 = (data: string): string =>
51
- fromByteArray(new Uint8Array(map(data, (char) => char.charCodeAt(0))));
52
-
53
- // base-64 decoder throws exception if encoded string is not padded by '=' to make string length
54
- // in multiples of 4. So gonna use our own method for this purpose to keep backwards compatibility
55
- // https://github.com/beatgammit/base64-js/blob/master/index.js#L26
56
- export const decodeBase64 = (s: string): string => {
57
- const e = {} as { [key: string]: number },
58
- w = String.fromCharCode,
59
- L = s.length;
60
- let i,
61
- b = 0,
62
- c,
63
- x,
64
- l = 0,
65
- a,
66
- r = '';
67
- const A = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
68
- for (i = 0; i < 64; i++) {
69
- e[A.charAt(i)] = i;
70
- }
71
- for (x = 0; x < L; x++) {
72
- c = e[s.charAt(x)];
73
- b = (b << 6) + c;
74
- l += 6;
75
- while (l >= 8) {
76
- ((a = (b >>> (l -= 8)) & 0xff) || x < L - 2) && (r += w(a));
77
- }
78
- }
79
- return r;
80
- };
@@ -1,242 +0,0 @@
1
- import axios, { AxiosRequestConfig, CancelTokenSource } from 'axios';
2
- import { StreamClient } from './client';
3
- import {
4
- addConnectionEventListeners,
5
- removeConnectionEventListeners,
6
- retryInterval,
7
- sleep,
8
- } from './utils';
9
- import { isAPIError, isConnectionIDError, isErrorRetryable } from './errors';
10
- import { LogLevel, StreamVideoEvent, UR } from './types';
11
- import { ConnectedEvent } from '../../gen/coordinator';
12
-
13
- export enum ConnectionState {
14
- Closed = 'CLOSED',
15
- Connected = 'CONNECTED',
16
- Connecting = 'CONNECTING',
17
- Disconnected = 'DISCONNECTED',
18
- Init = 'INIT',
19
- }
20
-
21
- export class WSConnectionFallback {
22
- client: StreamClient;
23
- state: ConnectionState;
24
- consecutiveFailures: number;
25
- connectionID?: string;
26
- cancelToken?: CancelTokenSource;
27
-
28
- constructor(client: StreamClient) {
29
- this.client = client;
30
- this.state = ConnectionState.Init;
31
- this.consecutiveFailures = 0;
32
-
33
- addConnectionEventListeners(this._onlineStatusChanged);
34
- }
35
-
36
- _log(msg: string, extra: UR = {}, level: LogLevel = 'info') {
37
- this.client.logger(level, 'WSConnectionFallback:' + msg, {
38
- ...extra,
39
- });
40
- }
41
-
42
- _setState(state: ConnectionState) {
43
- this._log(`_setState() - ${state}`);
44
-
45
- // transition from connecting => connected
46
- if (
47
- this.state === ConnectionState.Connecting &&
48
- state === ConnectionState.Connected
49
- ) {
50
- this.client.dispatchEvent({ type: 'connection.changed', online: true });
51
- }
52
-
53
- if (
54
- state === ConnectionState.Closed ||
55
- state === ConnectionState.Disconnected
56
- ) {
57
- this.client.dispatchEvent({ type: 'connection.changed', online: false });
58
- }
59
-
60
- this.state = state;
61
- }
62
-
63
- /** @private */
64
- _onlineStatusChanged = (event: { type: string }) => {
65
- this._log(`_onlineStatusChanged() - ${event.type}`);
66
-
67
- if (event.type === 'offline') {
68
- this._setState(ConnectionState.Closed);
69
- this.cancelToken?.cancel('disconnect() is called');
70
- this.cancelToken = undefined;
71
- return;
72
- }
73
-
74
- if (event.type === 'online' && this.state === ConnectionState.Closed) {
75
- this.connect(true);
76
- }
77
- };
78
-
79
- /** @private */
80
- _req = async <T = UR>(
81
- params: UR,
82
- config: AxiosRequestConfig,
83
- retry: boolean,
84
- ): Promise<T> => {
85
- if (!this.cancelToken && !params.close) {
86
- this.cancelToken = axios.CancelToken.source();
87
- }
88
-
89
- try {
90
- const res = await this.client.doAxiosRequest<T>(
91
- 'get',
92
- (this.client.baseURL as string).replace(':3030', ':8900') + '/longpoll', // replace port if present for testing with local API
93
- undefined,
94
- {
95
- config: { ...config, cancelToken: this.cancelToken?.token },
96
- params,
97
- publicEndpoint: true,
98
- },
99
- );
100
-
101
- this.consecutiveFailures = 0; // always reset in case of no error
102
- return res;
103
- } catch (err) {
104
- this.consecutiveFailures += 1;
105
-
106
- // @ts-ignore
107
- if (retry && isErrorRetryable(err)) {
108
- this._log(`_req() - Retryable error, retrying request`);
109
- await sleep(retryInterval(this.consecutiveFailures));
110
- return this._req<T>(params, config, retry);
111
- }
112
-
113
- throw err;
114
- }
115
- };
116
-
117
- /** @private */
118
- _poll = async () => {
119
- while (this.state === ConnectionState.Connected) {
120
- try {
121
- const data = await this._req<{
122
- events: StreamVideoEvent[];
123
- }>(
124
- {},
125
- {
126
- timeout: 30000,
127
- },
128
- true,
129
- ); // 30s => API responds in 20s if there is no event
130
-
131
- if (data.events?.length) {
132
- for (let i = 0; i < data.events.length; i++) {
133
- this.client.dispatchEvent(data.events[i]);
134
- }
135
- }
136
- } catch (err) {
137
- if (axios.isCancel(err)) {
138
- this._log(`_poll() - axios canceled request`);
139
- return;
140
- }
141
-
142
- /** client.doAxiosRequest will take care of TOKEN_EXPIRED error */
143
-
144
- // @ts-ignore
145
- if (isConnectionIDError(err)) {
146
- this._log(`_poll() - ConnectionID error, connecting without ID...`);
147
- this._setState(ConnectionState.Disconnected);
148
- this.connect(true);
149
- return;
150
- }
151
-
152
- // @ts-ignore
153
- if (isAPIError(err) && !isErrorRetryable(err)) {
154
- this._setState(ConnectionState.Closed);
155
- return;
156
- }
157
-
158
- await sleep(retryInterval(this.consecutiveFailures));
159
- }
160
- }
161
- };
162
-
163
- /**
164
- * connect try to open a longpoll request
165
- * @param reconnect should be false for first call and true for subsequent calls to keep the connection alive and call recoverState
166
- */
167
- connect = async (reconnect = false) => {
168
- if (this.state === ConnectionState.Connecting) {
169
- this._log(
170
- 'connect() - connecting already in progress',
171
- { reconnect },
172
- 'warn',
173
- );
174
- return;
175
- }
176
- if (this.state === ConnectionState.Connected) {
177
- this._log(
178
- 'connect() - already connected and polling',
179
- { reconnect },
180
- 'warn',
181
- );
182
- return;
183
- }
184
-
185
- this._setState(ConnectionState.Connecting);
186
- this.connectionID = undefined; // connect should be sent with empty connection_id so API creates one
187
- try {
188
- const { event } = await this._req<{
189
- event: ConnectedEvent;
190
- }>(
191
- { json: this.client._buildWSPayload() },
192
- {
193
- timeout: 8000, // 8s
194
- },
195
- reconnect,
196
- );
197
-
198
- this._setState(ConnectionState.Connected);
199
- this.connectionID = event.connection_id;
200
- this.client.resolveConnectionId?.();
201
- // @ts-expect-error
202
- this.client.dispatchEvent(event);
203
- this._poll();
204
- return event;
205
- } catch (err) {
206
- this._setState(ConnectionState.Closed);
207
- this.client.rejectConnectionId?.();
208
- throw err;
209
- }
210
- };
211
-
212
- /**
213
- * isHealthy checks if there is a connectionID and connection is in Connected state
214
- */
215
- isHealthy = () => {
216
- return !!this.connectionID && this.state === ConnectionState.Connected;
217
- };
218
-
219
- disconnect = async (timeout = 2000) => {
220
- removeConnectionEventListeners(this._onlineStatusChanged);
221
-
222
- this._setState(ConnectionState.Disconnected);
223
- this.cancelToken?.cancel('disconnect() is called');
224
- this.cancelToken = undefined;
225
-
226
- const connection_id = this.connectionID;
227
- this.connectionID = undefined;
228
-
229
- try {
230
- await this._req(
231
- { close: true, connection_id },
232
- {
233
- timeout,
234
- },
235
- false,
236
- );
237
- this._log(`disconnect() - Closed connectionID`);
238
- } catch (err) {
239
- this._log(`disconnect() - Failed`, { err }, 'error');
240
- }
241
- };
242
- }
@@ -1,80 +0,0 @@
1
- import { AxiosResponse } from 'axios';
2
- import { APIErrorResponse } from './types';
3
-
4
- export const APIErrorCodes: Record<
5
- string,
6
- { name: string; retryable: boolean }
7
- > = {
8
- '-1': { name: 'InternalSystemError', retryable: true },
9
- '2': { name: 'AccessKeyError', retryable: false },
10
- '3': { name: 'AuthenticationFailedError', retryable: true },
11
- '4': { name: 'InputError', retryable: false },
12
- '6': { name: 'DuplicateUsernameError', retryable: false },
13
- '9': { name: 'RateLimitError', retryable: true },
14
- '16': { name: 'DoesNotExistError', retryable: false },
15
- '17': { name: 'NotAllowedError', retryable: false },
16
- '18': { name: 'EventNotSupportedError', retryable: false },
17
- '19': { name: 'ChannelFeatureNotSupportedError', retryable: false },
18
- '20': { name: 'MessageTooLongError', retryable: false },
19
- '21': { name: 'MultipleNestingLevelError', retryable: false },
20
- '22': { name: 'PayloadTooBigError', retryable: false },
21
- '23': { name: 'RequestTimeoutError', retryable: true },
22
- '24': { name: 'MaxHeaderSizeExceededError', retryable: false },
23
- '40': { name: 'AuthErrorTokenExpired', retryable: false },
24
- '41': { name: 'AuthErrorTokenNotValidYet', retryable: false },
25
- '42': { name: 'AuthErrorTokenUsedBeforeIssuedAt', retryable: false },
26
- '43': { name: 'AuthErrorTokenSignatureInvalid', retryable: false },
27
- '44': { name: 'CustomCommandEndpointMissingError', retryable: false },
28
- '45': { name: 'CustomCommandEndpointCallError', retryable: true },
29
- '46': { name: 'ConnectionIDNotFoundError', retryable: false },
30
- '60': { name: 'CoolDownError', retryable: true },
31
- '69': { name: 'ErrWrongRegion', retryable: false },
32
- '70': { name: 'ErrQueryChannelPermissions', retryable: false },
33
- '71': { name: 'ErrTooManyConnections', retryable: true },
34
- '99': { name: 'AppSuspendedError', retryable: false },
35
- };
36
-
37
- // todo: this is not a correct error declaration. /recordings endpoint returns error objects as follows:
38
- // {
39
- // "code": 16,
40
- // "message": "ListRecordings failed with error: \"Can't find call with id default:bbbb\"",
41
- // "StatusCode": 404,
42
- // "duration": "0.00ms",
43
- // "more_info": "https://getstream.io/chat/docs/api_errors_response",
44
- // "details": []
45
- // }
46
-
47
- type APIError = Error & { code: number; isWSFailure?: boolean };
48
-
49
- export function isAPIError(error: Error): error is APIError {
50
- return (error as APIError).code !== undefined;
51
- }
52
-
53
- export function isErrorRetryable(error: APIError) {
54
- if (!error.code) return false;
55
- const err = APIErrorCodes[`${error.code}`];
56
- if (!err) return false;
57
- return err.retryable;
58
- }
59
-
60
- export function isConnectionIDError(error: APIError) {
61
- return error.code === 46; // ConnectionIDNotFoundError
62
- }
63
-
64
- export function isWSFailure(err: APIError): boolean {
65
- if (typeof err.isWSFailure === 'boolean') {
66
- return err.isWSFailure;
67
- }
68
-
69
- try {
70
- return JSON.parse(err.message).isWSFailure;
71
- } catch (_) {
72
- return false;
73
- }
74
- }
75
-
76
- export function isErrorResponse(
77
- res: AxiosResponse<unknown>,
78
- ): res is AxiosResponse<APIErrorResponse> {
79
- return !res.status || res.status < 200 || 300 <= res.status;
80
- }
@@ -1,88 +0,0 @@
1
- import axios from 'axios';
2
- import { StableWSConnection } from './connection';
3
- import { randomId, sleep } from './utils';
4
-
5
- export type InsightTypes =
6
- | 'ws_fatal'
7
- | 'ws_success_after_failure'
8
- | 'http_hi_failed';
9
- export class InsightMetrics {
10
- connectionStartTimestamp: number | null;
11
- wsConsecutiveFailures: number;
12
- wsTotalFailures: number;
13
- instanceClientId: string;
14
-
15
- constructor() {
16
- this.connectionStartTimestamp = null;
17
- this.wsTotalFailures = 0;
18
- this.wsConsecutiveFailures = 0;
19
- this.instanceClientId = randomId();
20
- }
21
- }
22
-
23
- /**
24
- * postInsights is not supposed to be used by end users directly within chat application, and thus is kept isolated
25
- * from all the client/connection code/logic.
26
- *
27
- * @param insightType
28
- * @param insights
29
- */
30
- export const postInsights = async (
31
- insightType: InsightTypes,
32
- insights: Record<string, unknown>,
33
- ) => {
34
- const maxAttempts = 3;
35
- for (let i = 0; i < maxAttempts; i++) {
36
- try {
37
- await axios.post(
38
- `https://chat-insights.getstream.io/insights/${insightType}`,
39
- insights,
40
- );
41
- } catch (e) {
42
- await sleep((i + 1) * 3000);
43
- continue;
44
- }
45
- break;
46
- }
47
- };
48
-
49
- export function buildWsFatalInsight(
50
- connection: StableWSConnection,
51
- event: Record<string, unknown>,
52
- ) {
53
- return {
54
- ...event,
55
- ...buildWsBaseInsight(connection),
56
- };
57
- }
58
-
59
- function buildWsBaseInsight(connection: StableWSConnection) {
60
- const { client } = connection;
61
- return {
62
- ready_state: connection.ws?.readyState,
63
- url: connection._buildUrl(),
64
- api_key: client.key,
65
- start_ts: client.insightMetrics.connectionStartTimestamp,
66
- end_ts: new Date().getTime(),
67
- auth_type: client.getAuthType(),
68
- token: client.tokenManager.token,
69
- user_id: client.userID,
70
- user_details: client._user,
71
- // device: client.options.device,
72
- device: 'browser',
73
- client_id: connection.connectionID,
74
- ws_details: connection.ws,
75
- ws_consecutive_failures: client.insightMetrics.wsConsecutiveFailures,
76
- ws_total_failures: client.insightMetrics.wsTotalFailures,
77
- request_id: connection.requestID,
78
- online: typeof navigator !== 'undefined' ? navigator?.onLine : null,
79
- user_agent: typeof navigator !== 'undefined' ? navigator?.userAgent : null,
80
- instance_client_id: client.insightMetrics.instanceClientId,
81
- };
82
- }
83
-
84
- export function buildWsSuccessAfterFailureInsight(
85
- connection: StableWSConnection,
86
- ) {
87
- return buildWsBaseInsight(connection);
88
- }