Skip to content

Commit 0aefda0

Browse files
committed
Merge branch 'main' into worker-timers
2 parents e5d605e + 2695bab commit 0aefda0

File tree

239 files changed

+10587
-5980
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

239 files changed

+10587
-5980
lines changed

.github/workflows/deploy-react-sample-apps.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ jobs:
7272
- name: Setup Node
7373
uses: actions/setup-node@v4
7474
with:
75-
node-version: 20.x
75+
node-version: 22.x
7676
cache: 'yarn'
7777

7878
- name: Install Dependencies

.github/workflows/egress-composite-e2e.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ jobs:
2828
- name: Setup Node
2929
uses: actions/setup-node@v4
3030
with:
31-
node-version: 20.x
31+
node-version: 22.x
3232
cache: 'yarn'
3333
cache-dependency-path: 'yarn.lock'
3434

.github/workflows/react-native-workflow.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ jobs:
130130
name: Deploy iOS
131131
needs: build_ios
132132
timeout-minutes: 60
133-
if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/PBE-5855-feat/react-native-video-design-v2' }}
133+
if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/use-vp8-on-ios' }}
134134
runs-on: macos-latest
135135
steps:
136136
- uses: actions/checkout@v4

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ jobs:
2727
- name: Setup Node
2828
uses: actions/setup-node@v4
2929
with:
30-
node-version: 20.x
30+
node-version: 22.x
3131
cache: 'yarn'
3232

3333
- name: ESLint Cache

.github/workflows/version-and-release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ jobs:
2929
- name: Setup Node
3030
uses: actions/setup-node@v4
3131
with:
32-
node-version: 20.x
32+
node-version: 22.x
3333
cache: 'yarn'
3434

3535
- name: ESLint Cache

.nvmrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
v20
1+
v22

packages/client/CHANGELOG.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,62 @@
22

33
This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver).
44

5+
## [1.11.12](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.11.11...@stream-io/video-client-1.11.12) (2024-12-03)
6+
7+
8+
### Bug Fixes
9+
10+
* handle timeout on SFU WS connections ([#1600](https://github.com/GetStream/stream-video-js/issues/1600)) ([5f2db7b](https://github.com/GetStream/stream-video-js/commit/5f2db7bd5cfdf57cdc04d6a6ed752f43e5b06657))
11+
12+
## [1.11.11](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.11.10...@stream-io/video-client-1.11.11) (2024-11-29)
13+
14+
15+
### Bug Fixes
16+
17+
* revert [#1604](https://github.com/GetStream/stream-video-js/issues/1604) ([#1607](https://github.com/GetStream/stream-video-js/issues/1607)) ([567e4fb](https://github.com/GetStream/stream-video-js/commit/567e4fb309509b6b0d814826856d0a15efe16271))
18+
19+
## [1.11.10](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.11.9...@stream-io/video-client-1.11.10) (2024-11-28)
20+
21+
22+
### Bug Fixes
23+
24+
* ringing calls not being left when ended ([#1601](https://github.com/GetStream/stream-video-js/issues/1601)) ([1c2b9d1](https://github.com/GetStream/stream-video-js/commit/1c2b9d1a54767652acc52cae9bb3d348c9df566f))
25+
26+
## [1.11.9](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.11.8...@stream-io/video-client-1.11.9) (2024-11-27)
27+
28+
29+
### Bug Fixes
30+
31+
* cover some device selection edge cases ([#1604](https://github.com/GetStream/stream-video-js/issues/1604)) ([a8fc0ea](https://github.com/GetStream/stream-video-js/commit/a8fc0eaf1ed6c79ce24f77f52351a1e90701bd02))
32+
33+
## [1.11.8](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.11.7...@stream-io/video-client-1.11.8) (2024-11-27)
34+
35+
36+
### Bug Fixes
37+
38+
* **ios:** use vp8 when h264 constrainted baseline isn't available ([#1597](https://github.com/GetStream/stream-video-js/issues/1597)) ([6281216](https://github.com/GetStream/stream-video-js/commit/62812161cef5e9917c504dbc4cd9257709ea5fa1))
39+
40+
## [1.11.7](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.11.6...@stream-io/video-client-1.11.7) (2024-11-26)
41+
42+
43+
### Bug Fixes
44+
45+
* remove unused code from the coordinator websocket impl ([#1563](https://github.com/GetStream/stream-video-js/issues/1563)) ([921b820](https://github.com/GetStream/stream-video-js/commit/921b820133885dac299dab343cee3fc4b08705ce))
46+
47+
## [1.11.6](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.11.5...@stream-io/video-client-1.11.6) (2024-11-22)
48+
49+
50+
### Bug Fixes
51+
52+
* force single codec preference in the SDP ([#1588](https://github.com/GetStream/stream-video-js/issues/1588)) ([4afff09](https://github.com/GetStream/stream-video-js/commit/4afff09a778f8567176d22bcc22d36001dca7cd3)), closes [#1581](https://github.com/GetStream/stream-video-js/issues/1581)
53+
54+
## [1.11.5](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.11.4...@stream-io/video-client-1.11.5) (2024-11-22)
55+
56+
57+
### Bug Fixes
58+
59+
* unhandled promise rejections during reconnect ([#1585](https://github.com/GetStream/stream-video-js/issues/1585)) ([920c4ea](https://github.com/GetStream/stream-video-js/commit/920c4ea3b3f622430b35ac1bade74a6206ee17e5)), closes [/github.com/GetStream/stream-video-js/pull/1585/files#diff-420f6ddab47c1be72fd9ce8c99e1fa2b9f5f0495b7c367546ee0ff634beaed81](https://github.com/GetStream//github.com/GetStream/stream-video-js/pull/1585/files/issues/diff-420f6ddab47c1be72fd9ce8c99e1fa2b9f5f0495b7c367546ee0ff634beaed81)
60+
561
## [1.11.4](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.11.3...@stream-io/video-client-1.11.4) (2024-11-21)
662

763

packages/client/package.json

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@stream-io/video-client",
3-
"version": "1.11.4",
3+
"version": "1.11.12",
44
"packageManager": "[email protected]",
55
"main": "dist/index.cjs.js",
66
"module": "dist/index.es.js",
@@ -32,15 +32,11 @@
3232
"@protobuf-ts/runtime": "^2.9.4",
3333
"@protobuf-ts/runtime-rpc": "^2.9.4",
3434
"@protobuf-ts/twirp-transport": "^2.9.4",
35-
"@types/ws": "^8.5.7",
36-
"axios": "^1.6.0",
37-
"base64-js": "^1.5.1",
38-
"isomorphic-ws": "^5.0.0",
35+
"axios": "^1.7.7",
3936
"rxjs": "~7.8.1",
4037
"sdp-transform": "^2.14.1",
4138
"ua-parser-js": "^1.0.36",
42-
"webrtc-adapter": "^8.2.3",
43-
"ws": "^8.14.2"
39+
"webrtc-adapter": "^8.2.3"
4440
},
4541
"devDependencies": {
4642
"@openapitools/openapi-generator-cli": "^2.13.4",
@@ -50,15 +46,15 @@
5046
"@stream-io/node-sdk": "^0.4.3",
5147
"@types/sdp-transform": "^2.4.7",
5248
"@types/ua-parser-js": "^0.7.37",
53-
"@vitest/coverage-v8": "^0.34.4",
49+
"@vitest/coverage-v8": "^2.1.4",
5450
"dotenv": "^16.3.1",
5551
"happy-dom": "^11.0.2",
5652
"prettier": "^3.3.2",
5753
"rimraf": "^5.0.7",
5854
"rollup": "^4.22.0",
5955
"typescript": "^5.5.2",
6056
"vite": "^5.4.6",
61-
"vitest": "^1.0.0",
62-
"vitest-mock-extended": "^1.2.1"
57+
"vitest": "^2.1.4",
58+
"vitest-mock-extended": "^2.0.2"
6359
}
6460
}

packages/client/src/Call.ts

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,10 @@ import { getSdkSignature } from './stats/utils';
120120
import { withoutConcurrency } from './helpers/concurrency';
121121
import { ensureExhausted } from './helpers/ensureExhausted';
122122
import {
123+
makeSafePromise,
123124
PromiseWithResolvers,
124125
promiseWithResolvers,
125-
} from './helpers/withResolvers';
126+
} from './helpers/promise';
126127

127128
/**
128129
* An object representation of a `Call`.
@@ -213,6 +214,7 @@ export class Call {
213214
private reconnectAttempts = 0;
214215
private reconnectStrategy = WebsocketReconnectStrategy.UNSPECIFIED;
215216
private fastReconnectDeadlineSeconds: number = 0;
217+
private disconnectionTimeoutSeconds: number = 0;
216218
private lastOfflineTimestamp: number = 0;
217219
private networkAvailableTask: PromiseWithResolvers<void> | undefined;
218220
// maintain the order of publishing tracks to restore them after a reconnection
@@ -496,7 +498,7 @@ export class Call {
496498
* Leave the call and stop the media streams that were published by the call.
497499
*/
498500
leave = async ({
499-
reject = false,
501+
reject,
500502
reason = 'user is leaving the call',
501503
}: CallLeaveOptions = {}) => {
502504
await withoutConcurrency(this.joinLeaveConcurrencyTag, async () => {
@@ -516,13 +518,14 @@ export class Call {
516518
await waitUntilCallJoined();
517519
}
518520

519-
if (callingState === CallingState.RINGING) {
521+
if (callingState === CallingState.RINGING && reject !== false) {
520522
if (reject) {
521523
await this.reject(reason);
522524
} else {
525+
// if reject was undefined, we still have to cancel the call automatically
526+
// when I am the creator and everyone else left the call
523527
const hasOtherParticipants = this.state.remoteParticipants.length > 0;
524528
if (this.isCreatedByMe && !hasOtherParticipants) {
525-
// I'm the one who started the call, so I should cancel it when there are no other participants.
526529
await this.reject('cancel');
527530
}
528531
}
@@ -1050,6 +1053,9 @@ export class Call {
10501053
*/
10511054
private handleSfuSignalClose = (sfuClient: StreamSfuClient) => {
10521055
this.logger('debug', '[Reconnect] SFU signal connection closed');
1056+
// SFU WS closed before we finished current join, no need to schedule reconnect
1057+
// because join operation will fail
1058+
if (this.state.callingState === CallingState.JOINING) return;
10531059
// normal close, no need to reconnect
10541060
if (sfuClient.isLeaving) return;
10551061
this.reconnect(WebsocketReconnectStrategy.REJOIN).catch((err) => {
@@ -1067,14 +1073,35 @@ export class Call {
10671073
private reconnect = async (
10681074
strategy: WebsocketReconnectStrategy,
10691075
): Promise<void> => {
1076+
if (
1077+
this.state.callingState === CallingState.RECONNECTING ||
1078+
this.state.callingState === CallingState.RECONNECTING_FAILED
1079+
)
1080+
return;
1081+
10701082
return withoutConcurrency(this.reconnectConcurrencyTag, async () => {
10711083
this.logger(
10721084
'info',
10731085
`[Reconnect] Reconnecting with strategy ${WebsocketReconnectStrategy[strategy]}`,
10741086
);
10751087

1088+
let reconnectStartTime = Date.now();
10761089
this.reconnectStrategy = strategy;
1090+
10771091
do {
1092+
if (
1093+
this.disconnectionTimeoutSeconds > 0 &&
1094+
(Date.now() - reconnectStartTime) / 1000 >
1095+
this.disconnectionTimeoutSeconds
1096+
) {
1097+
this.logger(
1098+
'warn',
1099+
'[Reconnect] Stopping reconnection attempts after reaching disconnection timeout',
1100+
);
1101+
this.state.setCallingState(CallingState.RECONNECTING_FAILED);
1102+
return;
1103+
}
1104+
10781105
// we don't increment reconnect attempts for the FAST strategy.
10791106
if (this.reconnectStrategy !== WebsocketReconnectStrategy.FAST) {
10801107
this.reconnectAttempts++;
@@ -1192,7 +1219,7 @@ export class Call {
11921219
currentSubscriber?.detachEventHandlers();
11931220
currentPublisher?.detachEventHandlers();
11941221

1195-
const migrationTask = currentSfuClient.enterMigration();
1222+
const migrationTask = makeSafePromise(currentSfuClient.enterMigration());
11961223

11971224
try {
11981225
const currentSfu = currentSfuClient.edgeName;
@@ -1210,7 +1237,7 @@ export class Call {
12101237
// Wait for the migration to complete, then close the previous SFU client
12111238
// and the peer connection instances. In case of failure, the migration
12121239
// task would throw an error and REJOIN would be attempted.
1213-
await migrationTask;
1240+
await migrationTask();
12141241

12151242
// in MIGRATE, we can consider the call as joined only after
12161243
// `participantMigrationComplete` event is received, signaled by
@@ -2336,4 +2363,13 @@ export class Call {
23362363
);
23372364
this.dynascaleManager.applyTrackSubscriptions();
23382365
};
2366+
2367+
/**
2368+
* Sets the maximum amount of time a user can remain waiting for a reconnect
2369+
* after a network disruption
2370+
* @param timeoutSeconds Timeout in seconds, or 0 to keep reconnecting indefinetely
2371+
*/
2372+
setDisconnectionTimeout = (timeoutSeconds: number) => {
2373+
this.disconnectionTimeoutSeconds = timeoutSeconds;
2374+
};
23392375
}

packages/client/src/StreamSfuClient.ts

Lines changed: 27 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,16 @@ import {
2525
} from './gen/video/sfu/signal_rpc/signal';
2626
import { ICETrickle, TrackType } from './gen/video/sfu/models/models';
2727
import { StreamClient } from './coordinator/connection/client';
28-
import { generateUUIDv4, sleep } from './coordinator/connection/utils';
28+
import { generateUUIDv4 } from './coordinator/connection/utils';
2929
import { Credentials } from './gen/coordinator';
3030
import { Logger } from './coordinator/connection/types';
3131
import { getLogger, getLogLevel } from './logger';
32-
import { withoutConcurrency } from './helpers/concurrency';
3332
import {
3433
promiseWithResolvers,
3534
PromiseWithResolvers,
36-
} from './helpers/withResolvers';
35+
makeSafePromise,
36+
SafePromise,
37+
} from './helpers/promise';
3738
import { getTimers } from './timers';
3839

3940
export type StreamSfuClientConstructor = {
@@ -102,7 +103,7 @@ export class StreamSfuClient {
102103
/**
103104
* Promise that resolves when the WebSocket connection is ready (open).
104105
*/
105-
private signalReady!: Promise<WebSocket>;
106+
private signalReady!: SafePromise<WebSocket>;
106107

107108
/**
108109
* Flag to indicate if the client is in the process of leaving the call.
@@ -117,15 +118,14 @@ export class StreamSfuClient {
117118
private pingIntervalInMs = 10 * 1000;
118119
private unhealthyTimeoutInMs = this.pingIntervalInMs + 5 * 1000;
119120
private lastMessageTimestamp?: Date;
120-
private readonly restoreWebSocketConcurrencyTag = Symbol('recoverWebSocket');
121121
private readonly unsubscribeIceTrickle: () => void;
122122
private readonly unsubscribeNetworkChanged: () => void;
123123
private readonly onSignalClose: (() => void) | undefined;
124124
private readonly logger: Logger;
125125
private readonly logTag: string;
126126
private readonly credentials: Credentials;
127127
private readonly dispatcher: Dispatcher;
128-
private readonly joinResponseTimeout?: number;
128+
private readonly joinResponseTimeout: number;
129129
private networkAvailableTask: PromiseWithResolvers<void> | undefined;
130130
/**
131131
* Promise that resolves when the JoinResponse is received.
@@ -228,32 +228,31 @@ export class StreamSfuClient {
228228
});
229229

230230
this.signalWs.addEventListener('close', this.handleWebSocketClose);
231-
this.signalWs.addEventListener('error', this.restoreWebSocket);
232-
233-
this.signalReady = new Promise((resolve) => {
234-
const onOpen = () => {
235-
this.signalWs.removeEventListener('open', onOpen);
236-
resolve(this.signalWs);
237-
};
238-
this.signalWs.addEventListener('open', onOpen);
239-
});
231+
232+
this.signalReady = makeSafePromise(
233+
Promise.race<WebSocket>([
234+
new Promise((resolve) => {
235+
const onOpen = () => {
236+
this.signalWs.removeEventListener('open', onOpen);
237+
resolve(this.signalWs);
238+
};
239+
this.signalWs.addEventListener('open', onOpen);
240+
}),
241+
242+
new Promise((resolve, reject) => {
243+
setTimeout(
244+
() => reject(new Error('SFU WS connection timed out')),
245+
this.joinResponseTimeout,
246+
);
247+
}),
248+
]),
249+
);
240250
};
241251

242252
private cleanUpWebSocket = () => {
243-
this.signalWs.removeEventListener('error', this.restoreWebSocket);
244253
this.signalWs.removeEventListener('close', this.handleWebSocketClose);
245254
};
246255

247-
private restoreWebSocket = () => {
248-
withoutConcurrency(this.restoreWebSocketConcurrencyTag, async () => {
249-
await this.networkAvailableTask?.promise;
250-
this.logger('debug', 'Restoring SFU WS connection');
251-
this.cleanUpWebSocket();
252-
await sleep(500);
253-
this.createWebSocket();
254-
}).catch((err) => this.logger('debug', `Can't restore WS connection`, err));
255-
};
256-
257256
get isHealthy() {
258257
return this.signalWs.readyState === WebSocket.OPEN;
259258
}
@@ -410,7 +409,7 @@ export class StreamSfuClient {
410409
data: Omit<JoinRequest, 'sessionId' | 'token'>,
411410
): Promise<JoinResponse> => {
412411
// wait for the signal web socket to be ready before sending "joinRequest"
413-
await this.signalReady;
412+
await this.signalReady();
414413
if (this.joinResponseTask.isResolved || this.joinResponseTask.isRejected) {
415414
// we need to lock the RPC requests until we receive a JoinResponse.
416415
// that's why we have this primitive lock mechanism.
@@ -479,7 +478,7 @@ export class StreamSfuClient {
479478
};
480479

481480
private send = async (message: SfuRequest) => {
482-
await this.signalReady; // wait for the signal ws to be open
481+
await this.signalReady(); // wait for the signal ws to be open
483482
const msgJson = SfuRequest.toJson(message);
484483
if (this.signalWs.readyState !== WebSocket.OPEN) {
485484
this.logger('debug', 'Signal WS is not open. Skipping message', msgJson);

0 commit comments

Comments
 (0)