@stream-io/video-client 1.55.0 → 1.55.1
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/CHANGELOG.md +7 -0
- package/dist/index.browser.es.js +29 -9
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +29 -9
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +29 -9
- package/dist/index.es.js.map +1 -1
- package/dist/src/coordinator/connection/connection.d.ts +1 -0
- package/dist/src/rtc/types.d.ts +1 -0
- package/package.json +2 -2
- package/src/coordinator/connection/__tests__/connection.test.ts +47 -0
- package/src/coordinator/connection/connection.ts +20 -8
- package/src/rtc/Publisher.ts +7 -0
- package/src/rtc/__tests__/Publisher.test.ts +74 -0
- package/src/rtc/types.ts +1 -0
|
@@ -40,6 +40,7 @@ export declare class StableWSConnection {
|
|
|
40
40
|
consecutiveFailures: number;
|
|
41
41
|
/** keep track of the total number of failures */
|
|
42
42
|
totalFailures: number;
|
|
43
|
+
lastConnectionError?: WSConnectionError;
|
|
43
44
|
/** Send a health check message every 25 seconds */
|
|
44
45
|
pingInterval: number;
|
|
45
46
|
healthCheckTimeoutRef?: number;
|
package/dist/src/rtc/types.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stream-io/video-client",
|
|
3
|
-
"version": "1.55.
|
|
3
|
+
"version": "1.55.1",
|
|
4
4
|
"main": "dist/index.cjs.js",
|
|
5
5
|
"module": "dist/index.es.js",
|
|
6
6
|
"browser": "dist/index.browser.es.js",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"@openapitools/openapi-generator-cli": "^2.34.0",
|
|
47
47
|
"@rollup/plugin-replace": "^6.0.3",
|
|
48
48
|
"@rollup/plugin-typescript": "^12.3.0",
|
|
49
|
-
"@stream-io/audio-filters-web": "^0.9.
|
|
49
|
+
"@stream-io/audio-filters-web": "^0.9.1",
|
|
50
50
|
"@stream-io/node-sdk": "^0.7.59",
|
|
51
51
|
"@stream-io/typescript-config": "^0.0.0",
|
|
52
52
|
"@total-typescript/shoehorn": "^0.1.2",
|
|
@@ -215,6 +215,53 @@ describe('StableWSConnection - silent handshake hang', () => {
|
|
|
215
215
|
expect(outcome.kind).toBe('rejected');
|
|
216
216
|
});
|
|
217
217
|
|
|
218
|
+
it('preserves an initial WS close reason when reconnect cannot get healthy', async () => {
|
|
219
|
+
const client = new StreamClient('test-key', {
|
|
220
|
+
browser: false,
|
|
221
|
+
defaultWsTimeout: 5000,
|
|
222
|
+
WebSocketImpl: ManualWebSocket as unknown as typeof WebSocket,
|
|
223
|
+
timeout: 1000,
|
|
224
|
+
});
|
|
225
|
+
vi.spyOn(client.tokenManager, 'tokenReady').mockResolvedValue('fake-token');
|
|
226
|
+
vi.spyOn(client.tokenManager, 'loadToken').mockResolvedValue('fake-token');
|
|
227
|
+
vi.spyOn(client.tokenManager, 'getToken').mockReturnValue('fake-token');
|
|
228
|
+
|
|
229
|
+
client._setUser({ id: 'test-user' });
|
|
230
|
+
client.userID = 'test-user';
|
|
231
|
+
client.clientID = 'test-user--abcdef';
|
|
232
|
+
client._setupConnectionIdPromise();
|
|
233
|
+
|
|
234
|
+
const wsConnection = new StableWSConnection(client);
|
|
235
|
+
client.wsConnection = wsConnection;
|
|
236
|
+
|
|
237
|
+
const connectAttemptOutcome = wsConnection.connect(5000).then(
|
|
238
|
+
() => ({ kind: 'resolved' as const }),
|
|
239
|
+
(error: Error) => ({ kind: 'rejected' as const, error }),
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
243
|
+
const ws = ManualWebSocket.instances.at(-1)!;
|
|
244
|
+
expect(ws).toBeDefined();
|
|
245
|
+
|
|
246
|
+
ws.onclose?.({
|
|
247
|
+
code: 1006,
|
|
248
|
+
reason: 'specific ws close reason',
|
|
249
|
+
wasClean: false,
|
|
250
|
+
target: ws,
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
await vi.advanceTimersByTimeAsync(20000);
|
|
254
|
+
const outcome = await connectAttemptOutcome;
|
|
255
|
+
|
|
256
|
+
expect(outcome.kind).toBe('rejected');
|
|
257
|
+
if (outcome.kind === 'rejected') {
|
|
258
|
+
expect(outcome.error.message).toContain('specific ws close reason');
|
|
259
|
+
expect(outcome.error.message).not.toContain(
|
|
260
|
+
'initial WS connection could not be established',
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
218
265
|
it('does not schedule a reconnect (and leaves connectionIdPromise rejected) on a permanent, non-WS failure', async () => {
|
|
219
266
|
const client = new StreamClient('test-key', {
|
|
220
267
|
browser: false,
|
|
@@ -66,6 +66,7 @@ export class StableWSConnection {
|
|
|
66
66
|
consecutiveFailures = 0;
|
|
67
67
|
/** keep track of the total number of failures */
|
|
68
68
|
totalFailures = 0;
|
|
69
|
+
lastConnectionError?: WSConnectionError;
|
|
69
70
|
|
|
70
71
|
// Health-check pings + connection-staleness check.
|
|
71
72
|
/** Send a health check message every 25 seconds */
|
|
@@ -102,6 +103,7 @@ export class StableWSConnection {
|
|
|
102
103
|
}
|
|
103
104
|
|
|
104
105
|
this.isDisconnected = false;
|
|
106
|
+
this.lastConnectionError = undefined;
|
|
105
107
|
|
|
106
108
|
try {
|
|
107
109
|
const healthCheck = await this._connect(timeout);
|
|
@@ -140,6 +142,7 @@ export class StableWSConnection {
|
|
|
140
142
|
// _connect()'s catch) keeps a single failure from spawning two
|
|
141
143
|
// parallel chains - one from this catch and one from _reconnect's
|
|
142
144
|
// own catch when _connect was called from there.
|
|
145
|
+
this.lastConnectionError = error;
|
|
143
146
|
this._reconnect();
|
|
144
147
|
}
|
|
145
148
|
}
|
|
@@ -179,14 +182,22 @@ export class StableWSConnection {
|
|
|
179
182
|
(async () => {
|
|
180
183
|
await sleep(timeout);
|
|
181
184
|
this.isConnecting = false;
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
185
|
+
const e = this.lastConnectionError;
|
|
186
|
+
const errorPayload = e
|
|
187
|
+
? {
|
|
188
|
+
code: e.code,
|
|
189
|
+
StatusCode: e.StatusCode,
|
|
190
|
+
message: e.message,
|
|
191
|
+
isWSFailure: e.isWSFailure,
|
|
192
|
+
}
|
|
193
|
+
: {
|
|
194
|
+
code: '',
|
|
195
|
+
StatusCode: '',
|
|
196
|
+
message: 'initial WS connection could not be established',
|
|
197
|
+
isWSFailure: true,
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
throw new Error(JSON.stringify(errorPayload));
|
|
190
201
|
})(),
|
|
191
202
|
]);
|
|
192
203
|
};
|
|
@@ -376,6 +387,7 @@ export class StableWSConnection {
|
|
|
376
387
|
return undefined;
|
|
377
388
|
} catch (caught) {
|
|
378
389
|
const err = caught as WSConnectionError;
|
|
390
|
+
this.lastConnectionError = err;
|
|
379
391
|
this.isConnecting = false;
|
|
380
392
|
this._log(`_connect() - Error - `, err);
|
|
381
393
|
// Reject THIS attempt's connection-id promise (P1) directly via the
|
package/src/rtc/Publisher.ts
CHANGED
|
@@ -187,6 +187,9 @@ export class Publisher extends BasePeerConnection {
|
|
|
187
187
|
if (isAudioTrackType(trackType)) {
|
|
188
188
|
await this.updateAudioPublishOptions(trackType, options);
|
|
189
189
|
}
|
|
190
|
+
if (track && !bundle.negotiated) {
|
|
191
|
+
await this.negotiate();
|
|
192
|
+
}
|
|
190
193
|
};
|
|
191
194
|
|
|
192
195
|
/**
|
|
@@ -490,6 +493,10 @@ export class Publisher extends BasePeerConnection {
|
|
|
490
493
|
|
|
491
494
|
const { sdp: answerSdp } = response;
|
|
492
495
|
await this.pc.setRemoteDescription({ type: 'answer', sdp: answerSdp });
|
|
496
|
+
|
|
497
|
+
for (const bundle of this.transceiverCache.items()) {
|
|
498
|
+
if (bundle.transceiver.sender.track) bundle.negotiated = true;
|
|
499
|
+
}
|
|
493
500
|
} catch (err) {
|
|
494
501
|
// negotiation failed, rollback to the previous state
|
|
495
502
|
if (this.pc.signalingState === 'have-local-offer') {
|
|
@@ -157,6 +157,7 @@ describe('Publisher', () => {
|
|
|
157
157
|
publishOption: publisher['publishOptions'][0],
|
|
158
158
|
transceiver,
|
|
159
159
|
options: {},
|
|
160
|
+
negotiated: true,
|
|
160
161
|
});
|
|
161
162
|
|
|
162
163
|
await publisher.publish(track, TrackType.VIDEO);
|
|
@@ -166,6 +167,75 @@ describe('Publisher', () => {
|
|
|
166
167
|
expect(transceiver.sender.replaceTrack).toHaveBeenCalledWith(clone);
|
|
167
168
|
expect(track.stop).toHaveBeenCalled();
|
|
168
169
|
});
|
|
170
|
+
|
|
171
|
+
it('should not renegotiate when reusing an already-negotiated transceiver', async () => {
|
|
172
|
+
const track = new MediaStreamTrack();
|
|
173
|
+
const clone = new MediaStreamTrack();
|
|
174
|
+
vi.spyOn(track, 'clone').mockReturnValue(clone);
|
|
175
|
+
|
|
176
|
+
const transceiver = new RTCRtpTransceiver();
|
|
177
|
+
// @ts-expect-error test setup
|
|
178
|
+
transceiver.sender.track = track;
|
|
179
|
+
publisher['transceiverCache'].add({
|
|
180
|
+
publishOption: publisher['publishOptions'][0],
|
|
181
|
+
transceiver,
|
|
182
|
+
options: {},
|
|
183
|
+
negotiated: true,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// @ts-expect-error - private method
|
|
187
|
+
const negotiateSpy = vi.spyOn(publisher, 'negotiate').mockResolvedValue();
|
|
188
|
+
|
|
189
|
+
await publisher.publish(track, TrackType.VIDEO);
|
|
190
|
+
|
|
191
|
+
expect(transceiver.sender.replaceTrack).toHaveBeenCalledWith(clone);
|
|
192
|
+
expect(negotiateSpy).not.toHaveBeenCalled();
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should renegotiate on republish when a previous negotiation never reached the SFU (SetPublisher timeout)', async () => {
|
|
196
|
+
const track = new MediaStreamTrack();
|
|
197
|
+
const transceiver = new RTCRtpTransceiver();
|
|
198
|
+
// @ts-expect-error test setup
|
|
199
|
+
transceiver.sender.track = track;
|
|
200
|
+
const bundle = {
|
|
201
|
+
publishOption: publisher['publishOptions'][0],
|
|
202
|
+
transceiver,
|
|
203
|
+
options: {},
|
|
204
|
+
negotiated: false,
|
|
205
|
+
};
|
|
206
|
+
publisher['transceiverCache'].add(bundle);
|
|
207
|
+
|
|
208
|
+
vi.spyOn(publisher['pc'], 'createOffer')
|
|
209
|
+
// @ts-expect-error TS picks up the wrong overload
|
|
210
|
+
.mockResolvedValue({ sdp: 'offer-sdp', type: 'offer' });
|
|
211
|
+
vi.spyOn(publisher['pc'], 'setLocalDescription').mockResolvedValue();
|
|
212
|
+
vi.spyOn(publisher['pc'], 'setRemoteDescription').mockResolvedValue();
|
|
213
|
+
vi.spyOn(publisher, 'getAnnouncedTracks').mockReturnValue([
|
|
214
|
+
// @ts-expect-error incomplete data
|
|
215
|
+
{ trackId: '123' },
|
|
216
|
+
]);
|
|
217
|
+
|
|
218
|
+
sfuClient.setPublisher = vi
|
|
219
|
+
.fn()
|
|
220
|
+
.mockRejectedValue(new Error('SetPublisherTimeout'));
|
|
221
|
+
await expect(publisher['negotiate']()).rejects.toThrow(
|
|
222
|
+
'SetPublisherTimeout',
|
|
223
|
+
);
|
|
224
|
+
expect(bundle.negotiated).toBe(false);
|
|
225
|
+
|
|
226
|
+
const clone = new MediaStreamTrack();
|
|
227
|
+
vi.spyOn(track, 'clone').mockReturnValue(clone);
|
|
228
|
+
sfuClient.setPublisher = vi
|
|
229
|
+
.fn()
|
|
230
|
+
.mockResolvedValue({ response: { sdp: 'answer-sdp' } });
|
|
231
|
+
|
|
232
|
+
await publisher.publish(track, TrackType.VIDEO);
|
|
233
|
+
|
|
234
|
+
expect(publisher['pc'].addTransceiver).not.toHaveBeenCalled();
|
|
235
|
+
expect(transceiver.sender.replaceTrack).toHaveBeenCalledWith(clone);
|
|
236
|
+
expect(sfuClient.setPublisher).toHaveBeenCalled();
|
|
237
|
+
expect(bundle.negotiated).toBe(true);
|
|
238
|
+
});
|
|
169
239
|
});
|
|
170
240
|
|
|
171
241
|
describe('Event Handling', () => {
|
|
@@ -1457,6 +1527,7 @@ describe('Publisher', () => {
|
|
|
1457
1527
|
publishOption: publisher['publishOptions'][0],
|
|
1458
1528
|
transceiver,
|
|
1459
1529
|
options: {},
|
|
1530
|
+
negotiated: true,
|
|
1460
1531
|
});
|
|
1461
1532
|
|
|
1462
1533
|
// stopping seeds the bundle's videoSender from the current encoder
|
|
@@ -1515,11 +1586,13 @@ describe('Publisher', () => {
|
|
|
1515
1586
|
publishOption: publisher['publishOptions'][0],
|
|
1516
1587
|
transceiver: vp8Transceiver,
|
|
1517
1588
|
options: {},
|
|
1589
|
+
negotiated: true,
|
|
1518
1590
|
});
|
|
1519
1591
|
publisher['transceiverCache'].add({
|
|
1520
1592
|
publishOption: publisher['publishOptions'][1],
|
|
1521
1593
|
transceiver: vp9Transceiver,
|
|
1522
1594
|
options: {},
|
|
1595
|
+
negotiated: true,
|
|
1523
1596
|
});
|
|
1524
1597
|
|
|
1525
1598
|
await publisher.stopTracks(TrackType.VIDEO);
|
|
@@ -1589,6 +1662,7 @@ describe('Publisher', () => {
|
|
|
1589
1662
|
publishOption,
|
|
1590
1663
|
transceiver,
|
|
1591
1664
|
options: {},
|
|
1665
|
+
negotiated: true,
|
|
1592
1666
|
});
|
|
1593
1667
|
|
|
1594
1668
|
// SFU sends a changePublishQuality while we are not publishing.
|