@stream-io/video-client 1.59.1 → 1.61.0
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 +18 -0
- package/dist/index.browser.es.js +744 -156
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +744 -155
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.es.js +744 -156
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +34 -8
- package/dist/src/coordinator/connection/types.d.ts +16 -0
- package/dist/src/events/call.d.ts +0 -11
- package/dist/src/gen/coordinator/index.d.ts +73 -0
- package/dist/src/reporting/ClientEventReporter.d.ts +11 -1
- package/dist/src/ringing/RingStatePoller.d.ts +46 -0
- package/dist/src/ringing/RingTimeout.d.ts +27 -0
- package/dist/src/ringing/index.d.ts +4 -0
- package/dist/src/ringing/reconcileRingState.d.ts +18 -0
- package/dist/src/ringing/resolveOwnRingOutcome.d.ts +32 -0
- package/dist/src/rtc/Publisher.d.ts +18 -0
- package/dist/src/rtc/TransceiverCache.d.ts +10 -0
- package/dist/src/rtc/e2ee/E2EEManager.d.ts +10 -1
- package/dist/src/stats/utils.d.ts +6 -0
- package/dist/src/store/CallState.d.ts +10 -1
- package/dist/src/types.d.ts +30 -1
- package/index.ts +1 -0
- package/package.json +1 -1
- package/src/Call.ts +272 -108
- package/src/StreamSfuClient.ts +6 -2
- package/src/__tests__/Call.autodrop.test.ts +116 -64
- package/src/__tests__/Call.getRingState.test.ts +87 -0
- package/src/__tests__/Call.lifecycle.test.ts +498 -5
- package/src/__tests__/Call.ringSettled.test.ts +71 -0
- package/src/__tests__/Call.ringingLifecycle.test.ts +242 -0
- package/src/__tests__/StreamSfuClient.test.ts +48 -0
- package/src/coordinator/connection/types.ts +19 -0
- package/src/events/__tests__/call.test.ts +2 -217
- package/src/events/call.ts +1 -73
- package/src/events/callEventHandlers.ts +12 -23
- package/src/gen/coordinator/index.ts +67 -0
- package/src/reporting/ClientEventReporter.ts +30 -4
- package/src/reporting/__tests__/ClientEventReporter.test.ts +184 -7
- package/src/ringing/RingStatePoller.ts +177 -0
- package/src/ringing/RingTimeout.ts +90 -0
- package/src/ringing/__tests__/RingStatePoller.test.ts +462 -0
- package/src/ringing/__tests__/RingTimeout.test.ts +199 -0
- package/src/ringing/__tests__/reconcileRingState.test.ts +330 -0
- package/src/ringing/__tests__/resolveOwnRingOutcome.test.ts +103 -0
- package/src/ringing/index.ts +4 -0
- package/src/ringing/reconcileRingState.ts +108 -0
- package/src/ringing/resolveOwnRingOutcome.ts +58 -0
- package/src/rtc/Publisher.ts +66 -12
- package/src/rtc/TransceiverCache.ts +17 -0
- package/src/rtc/__tests__/Publisher.test.ts +209 -0
- package/src/rtc/__tests__/mocks/webrtc.mocks.ts +1 -0
- package/src/rtc/e2ee/E2EEManager.ts +10 -1
- package/src/rtc/e2ee/EncryptionManager.ts +1 -1
- package/src/rtc/e2ee/SPEC.md +1 -1
- package/src/stats/__tests__/utils.test.ts +33 -0
- package/src/stats/utils.ts +15 -0
- package/src/store/CallState.ts +25 -0
- package/src/store/__tests__/CallState.test.ts +59 -0
- package/src/types.ts +37 -1
package/src/StreamSfuClient.ts
CHANGED
|
@@ -39,7 +39,8 @@ import {
|
|
|
39
39
|
} from './helpers/promise';
|
|
40
40
|
import { withoutConcurrency } from './helpers/concurrency';
|
|
41
41
|
import { getTimers } from './timers';
|
|
42
|
-
import {
|
|
42
|
+
import { getSdkInfo } from './helpers/client-details';
|
|
43
|
+
import { getStreamClientId, Tracer, TraceSlice } from './stats';
|
|
43
44
|
import { SfuJoinError, SfuTimeoutError } from './errors';
|
|
44
45
|
|
|
45
46
|
export type StreamSfuClientConstructor = {
|
|
@@ -271,7 +272,10 @@ export class StreamSfuClient {
|
|
|
271
272
|
this.rpc = createSignalClient({
|
|
272
273
|
baseUrl: server.url,
|
|
273
274
|
interceptors: [
|
|
274
|
-
withHeaders({
|
|
275
|
+
withHeaders({
|
|
276
|
+
Authorization: `Bearer ${token}`,
|
|
277
|
+
'X-Stream-Client': getStreamClientId(getSdkInfo()),
|
|
278
|
+
}),
|
|
275
279
|
this.tracer && withRequestTracer(this.tracer.trace),
|
|
276
280
|
this.logger.getLogLevel() === 'trace' && withRequestLogger(this.logger),
|
|
277
281
|
withTimeout(rpcRequestTimeout, this.tracer?.trace),
|
|
@@ -1,104 +1,156 @@
|
|
|
1
1
|
import '../rtc/__tests__/mocks/webrtc.mocks';
|
|
2
2
|
|
|
3
3
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { fromPartial } from '@total-typescript/shoehorn';
|
|
4
5
|
import { Call } from '../Call';
|
|
5
6
|
import { StreamClient } from '../coordinator/connection/client';
|
|
6
7
|
import { ClientEventReporter } from '../reporting';
|
|
7
8
|
import { generateUUIDv4 } from '../coordinator/connection/utils';
|
|
8
9
|
import { CallingState, StreamVideoWriteableStateStore } from '../store';
|
|
10
|
+
import { CallSettingsResponse } from '../gen/coordinator';
|
|
9
11
|
|
|
12
|
+
const TIMEOUT_MS = 30_000;
|
|
13
|
+
|
|
14
|
+
// The timeout's own behaviour is covered by `ringing/__tests__/RingTimeout`.
|
|
15
|
+
// This file covers the wiring: that a ringing call arms one and that leaving
|
|
16
|
+
// cancels it.
|
|
10
17
|
describe('Auto drop ringing calls', () => {
|
|
11
|
-
let call: Call;
|
|
12
18
|
const userId = 'jane';
|
|
19
|
+
let call: Call;
|
|
13
20
|
|
|
14
|
-
|
|
15
|
-
vi.useFakeTimers();
|
|
16
|
-
|
|
21
|
+
const ringingCall = () => {
|
|
17
22
|
const clientStore = new StreamVideoWriteableStateStore();
|
|
18
23
|
const streamClient = new StreamClient('abc');
|
|
19
|
-
|
|
24
|
+
const newCall = new Call({
|
|
20
25
|
type: 'test',
|
|
21
26
|
id: generateUUIDv4(),
|
|
22
27
|
streamClient,
|
|
23
28
|
clientEventReporter: new ClientEventReporter({ streamClient }),
|
|
24
|
-
clientStore
|
|
29
|
+
clientStore,
|
|
30
|
+
ringing: true,
|
|
25
31
|
});
|
|
26
32
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
console.log(`TEST: leave() called`);
|
|
36
|
-
});
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('caller should drop ringing calls after a timeout if no one accepted', async () => {
|
|
40
|
-
call.state['settingsSubject'].next({
|
|
41
|
-
// @ts-expect-error mocking only what we need for the test, we use fake timers, so undefined for timeout works
|
|
42
|
-
ring: {},
|
|
43
|
-
// @ts-expect-error mocking only what we need for the test
|
|
44
|
-
screensharing: {
|
|
45
|
-
enabled: false,
|
|
46
|
-
target_resolution: {
|
|
47
|
-
width: 100,
|
|
48
|
-
height: 100,
|
|
33
|
+
clientStore.setConnectedUser(fromPartial({ id: userId }));
|
|
34
|
+
newCall.state['createdBySubject'].next(fromPartial({ id: userId }));
|
|
35
|
+
newCall.state['settingsSubject'].next(
|
|
36
|
+
fromPartial<CallSettingsResponse>({
|
|
37
|
+
ring: {
|
|
38
|
+
auto_cancel_timeout_ms: TIMEOUT_MS,
|
|
39
|
+
incoming_call_timeout_ms: TIMEOUT_MS,
|
|
40
|
+
missed_call_timeout_ms: TIMEOUT_MS,
|
|
49
41
|
},
|
|
50
|
-
|
|
51
|
-
|
|
42
|
+
screensharing: {
|
|
43
|
+
enabled: false,
|
|
44
|
+
target_resolution: { width: 100, height: 100 },
|
|
45
|
+
},
|
|
46
|
+
}),
|
|
47
|
+
);
|
|
48
|
+
newCall.state['sessionSubject'].next(
|
|
49
|
+
fromPartial({
|
|
50
|
+
id: 'session-1',
|
|
51
|
+
accepted_by: {},
|
|
52
|
+
rejected_by: {},
|
|
53
|
+
missed_by: {},
|
|
54
|
+
}),
|
|
55
|
+
);
|
|
56
|
+
newCall.state['callingStateSubject'].next(CallingState.RINGING);
|
|
57
|
+
|
|
58
|
+
vi.spyOn(newCall, 'leave').mockResolvedValue(undefined);
|
|
59
|
+
return newCall;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
vi.useFakeTimers();
|
|
64
|
+
call = ringingCall();
|
|
65
|
+
});
|
|
52
66
|
|
|
53
|
-
|
|
54
|
-
call
|
|
55
|
-
|
|
56
|
-
|
|
67
|
+
afterEach(() => {
|
|
68
|
+
call['cancelAutoDrop']();
|
|
69
|
+
vi.useRealTimers();
|
|
70
|
+
vi.restoreAllMocks();
|
|
71
|
+
});
|
|
57
72
|
|
|
58
|
-
|
|
59
|
-
call['
|
|
73
|
+
it('is armed when a call starts ringing', async () => {
|
|
74
|
+
call.state['callingStateSubject'].next(CallingState.IDLE);
|
|
60
75
|
|
|
61
|
-
|
|
76
|
+
call['handleRingingCall']();
|
|
62
77
|
|
|
78
|
+
expect(call['ringTimeout']).toBeDefined();
|
|
79
|
+
await vi.advanceTimersByTimeAsync(TIMEOUT_MS);
|
|
63
80
|
expect(call.leave).toHaveBeenCalledWith({
|
|
64
81
|
reject: true,
|
|
65
82
|
reason: 'timeout',
|
|
66
|
-
message:
|
|
83
|
+
message: 'ringing timeout - no one accepted',
|
|
67
84
|
});
|
|
68
85
|
});
|
|
69
86
|
|
|
70
|
-
it(
|
|
71
|
-
call
|
|
72
|
-
// @ts-expect-error mocking only what we need for the test, we use fake timers, so undefined for timeout works
|
|
73
|
-
ring: {},
|
|
74
|
-
// @ts-expect-error mocking only what we need for the test
|
|
75
|
-
screensharing: {
|
|
76
|
-
enabled: false,
|
|
77
|
-
target_resolution: {
|
|
78
|
-
width: 100,
|
|
79
|
-
height: 100,
|
|
80
|
-
},
|
|
81
|
-
},
|
|
82
|
-
});
|
|
87
|
+
it('is cancelled by cancelAutoDrop', async () => {
|
|
88
|
+
call['scheduleAutoDrop']();
|
|
83
89
|
|
|
84
|
-
|
|
85
|
-
call.state['createdBySubject'].next({
|
|
86
|
-
id: 'not-' + userId,
|
|
87
|
-
});
|
|
90
|
+
call['cancelAutoDrop']();
|
|
88
91
|
|
|
89
|
-
|
|
92
|
+
expect(call['ringTimeout']).toBeUndefined();
|
|
93
|
+
await vi.advanceTimersByTimeAsync(TIMEOUT_MS);
|
|
94
|
+
expect(call.leave).not.toHaveBeenCalled();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// the calling state stays RINGING well into teardown, so the watchdogs have
|
|
98
|
+
// to be paused before `leave` awaits anything
|
|
99
|
+
it('pauses the watchdogs synchronously when leave starts', async () => {
|
|
90
100
|
call['scheduleAutoDrop']();
|
|
101
|
+
call['scheduleRingStatePolling']();
|
|
102
|
+
const timeout = call['ringTimeout'];
|
|
103
|
+
vi.spyOn(call, 'leave').mockRestore();
|
|
104
|
+
vi.spyOn(call, 'reject').mockResolvedValue(fromPartial({}));
|
|
91
105
|
|
|
92
|
-
|
|
106
|
+
const poller = call['ringStatePoller'];
|
|
93
107
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
108
|
+
const leaving = call.leave({ reject: false });
|
|
109
|
+
|
|
110
|
+
// paused, not cancelled: both keep the window this leave is ending
|
|
111
|
+
expect(call['ringTimeout']).toBe(timeout);
|
|
112
|
+
expect(call['ringStatePoller']).toBe(poller);
|
|
113
|
+
expect(timeout!['timeoutId']).toBeUndefined();
|
|
114
|
+
expect(timeout!['stopped']).toBe(false);
|
|
115
|
+
expect(poller!['idleTimeoutId']).toBeUndefined();
|
|
116
|
+
expect(poller!['stopped']).toBe(false);
|
|
117
|
+
|
|
118
|
+
await leaving.catch(() => {});
|
|
119
|
+
|
|
120
|
+
expect(timeout!['stopped']).toBe(true);
|
|
121
|
+
expect(poller!['stopped']).toBe(true);
|
|
99
122
|
});
|
|
100
123
|
|
|
101
|
-
|
|
102
|
-
|
|
124
|
+
it('restores both watchdogs when rejecting the ring fails', async () => {
|
|
125
|
+
call['scheduleAutoDrop']();
|
|
126
|
+
call['scheduleRingStatePolling']();
|
|
127
|
+
const timeout = call['ringTimeout'];
|
|
128
|
+
const deadlineAt = timeout!['deadlineAt'];
|
|
129
|
+
const poller = call['ringStatePoller'];
|
|
130
|
+
const pollDeadlineAt = poller!['deadlineAt'];
|
|
131
|
+
vi.spyOn(call, 'leave').mockRestore();
|
|
132
|
+
vi.spyOn(call, 'reject').mockRejectedValueOnce(new Error('transient'));
|
|
133
|
+
|
|
134
|
+
await expect(call.leave({ reject: true })).rejects.toThrow('transient');
|
|
135
|
+
|
|
136
|
+
expect(call.state.callingState).toBe(CallingState.RINGING);
|
|
137
|
+
expect(call['ringTimeout']).toBe(timeout);
|
|
138
|
+
expect(timeout!['timeoutId']).toBeDefined();
|
|
139
|
+
expect(timeout!['deadlineAt']).toBe(deadlineAt);
|
|
140
|
+
expect(call['ringStatePoller']).toBe(poller);
|
|
141
|
+
expect(poller!['deadlineAt']).toBe(pollDeadlineAt);
|
|
142
|
+
expect(poller!['stopped']).toBe(false);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('replaces a previously armed timeout', async () => {
|
|
146
|
+
call['scheduleAutoDrop']();
|
|
147
|
+
const first = call['ringTimeout'];
|
|
148
|
+
|
|
149
|
+
call['scheduleAutoDrop']();
|
|
150
|
+
|
|
151
|
+
expect(call['ringTimeout']).not.toBe(first);
|
|
152
|
+
expect(first!['stopped']).toBe(true);
|
|
153
|
+
await vi.advanceTimersByTimeAsync(TIMEOUT_MS);
|
|
154
|
+
expect(call.leave).toHaveBeenCalledTimes(1);
|
|
103
155
|
});
|
|
104
156
|
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import '../rtc/__tests__/mocks/webrtc.mocks';
|
|
2
|
+
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { fromPartial } from '@total-typescript/shoehorn';
|
|
5
|
+
import { Call } from '../Call';
|
|
6
|
+
import { StreamClient } from '../coordinator/connection/client';
|
|
7
|
+
import { ClientEventReporter } from '../reporting';
|
|
8
|
+
import { generateUUIDv4 } from '../coordinator/connection/utils';
|
|
9
|
+
import { StreamVideoWriteableStateStore } from '../store';
|
|
10
|
+
import { CallSessionResponse } from '../gen/coordinator';
|
|
11
|
+
|
|
12
|
+
describe('Call.getRingState', () => {
|
|
13
|
+
const callId = generateUUIDv4();
|
|
14
|
+
|
|
15
|
+
const fakeCall = (sessionId?: string) => {
|
|
16
|
+
const streamClient = new StreamClient('abc');
|
|
17
|
+
const call = new Call({
|
|
18
|
+
type: 'test',
|
|
19
|
+
id: callId,
|
|
20
|
+
streamClient,
|
|
21
|
+
clientEventReporter: new ClientEventReporter({ streamClient }),
|
|
22
|
+
clientStore: new StreamVideoWriteableStateStore(),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
if (sessionId) {
|
|
26
|
+
call.state['sessionSubject'].next(
|
|
27
|
+
fromPartial<CallSessionResponse>({ id: sessionId }),
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const get = vi
|
|
32
|
+
.spyOn(streamClient, 'get')
|
|
33
|
+
.mockResolvedValue(fromPartial({ session_id: sessionId }));
|
|
34
|
+
|
|
35
|
+
return { call, get };
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
vi.restoreAllMocks();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('reads the ring state of the current session', async () => {
|
|
43
|
+
const { call, get } = fakeCall('session-1');
|
|
44
|
+
|
|
45
|
+
await call.getRingState();
|
|
46
|
+
|
|
47
|
+
expect(get).toHaveBeenCalledWith(`/call/test/${callId}/ring_state`, {
|
|
48
|
+
call_session_id: 'session-1',
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('reads the ring state of an explicitly named session', async () => {
|
|
53
|
+
// ending a call clears its current session, so a caller reconciling after
|
|
54
|
+
// `call.ended` has to name the session it rang on
|
|
55
|
+
const { call, get } = fakeCall('current-session');
|
|
56
|
+
|
|
57
|
+
await call.getRingState('ended-session');
|
|
58
|
+
|
|
59
|
+
expect(get).toHaveBeenCalledWith(`/call/test/${callId}/ring_state`, {
|
|
60
|
+
call_session_id: 'ended-session',
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('returns the coordinator response', async () => {
|
|
65
|
+
const { call, get } = fakeCall('session-1');
|
|
66
|
+
get.mockResolvedValue(
|
|
67
|
+
fromPartial({
|
|
68
|
+
session_id: 'session-1',
|
|
69
|
+
accepted_by: { bob: '2026-08-24T10:00:04Z' },
|
|
70
|
+
}),
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
await expect(call.getRingState()).resolves.toMatchObject({
|
|
74
|
+
session_id: 'session-1',
|
|
75
|
+
accepted_by: { bob: '2026-08-24T10:00:04Z' },
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('rejects when the call has no session to read', async () => {
|
|
80
|
+
const { call, get } = fakeCall();
|
|
81
|
+
|
|
82
|
+
await expect(call.getRingState()).rejects.toThrow(
|
|
83
|
+
'Cannot read the ring state: the call has no session',
|
|
84
|
+
);
|
|
85
|
+
expect(get).not.toHaveBeenCalled();
|
|
86
|
+
});
|
|
87
|
+
});
|