@stream-io/video-client 1.49.0 → 1.50.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 +11 -0
- package/dist/index.browser.es.js +1086 -594
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +1086 -594
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +1086 -594
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +42 -3
- package/dist/src/coordinator/connection/client.d.ts +1 -1
- package/dist/src/coordinator/connection/connection.d.ts +31 -25
- package/dist/src/coordinator/connection/types.d.ts +14 -0
- package/dist/src/coordinator/connection/utils.d.ts +1 -0
- package/dist/src/devices/DeviceManager.d.ts +3 -0
- package/dist/src/devices/DeviceManagerState.d.ts +0 -1
- package/dist/src/gen/video/sfu/event/events.d.ts +5 -1
- package/dist/src/gen/video/sfu/models/models.d.ts +34 -0
- package/dist/src/helpers/AudioBindingsWatchdog.d.ts +3 -3
- package/dist/src/helpers/BlockedAudioTracker.d.ts +30 -0
- package/dist/src/helpers/DynascaleManager.d.ts +8 -86
- package/dist/src/helpers/MediaPlaybackWatchdog.d.ts +32 -0
- package/dist/src/helpers/TrackSubscriptionManager.d.ts +114 -0
- package/dist/src/helpers/ViewportTracker.d.ts +11 -17
- package/dist/src/helpers/browsers.d.ts +13 -0
- package/dist/src/helpers/concurrency.d.ts +6 -4
- package/dist/src/rtc/Publisher.d.ts +17 -0
- package/dist/src/rtc/Subscriber.d.ts +1 -0
- package/dist/src/rtc/helpers/degradationPreference.d.ts +2 -0
- package/dist/src/stats/rtc/types.d.ts +1 -1
- package/dist/src/store/rxUtils.d.ts +9 -0
- package/dist/src/types.d.ts +18 -0
- package/package.json +2 -2
- package/src/Call.ts +89 -22
- package/src/__tests__/Call.lifecycle.test.ts +67 -0
- package/src/coordinator/connection/__tests__/connection.test.ts +482 -0
- package/src/coordinator/connection/client.ts +1 -1
- package/src/coordinator/connection/connection.ts +149 -96
- package/src/coordinator/connection/types.ts +15 -0
- package/src/coordinator/connection/utils.ts +15 -0
- package/src/devices/DeviceManager.ts +92 -32
- package/src/devices/DeviceManagerState.ts +0 -1
- package/src/devices/__tests__/DeviceManager.test.ts +283 -0
- package/src/devices/__tests__/mocks.ts +2 -0
- package/src/gen/video/sfu/event/events.ts +15 -0
- package/src/gen/video/sfu/models/models.ts +44 -0
- package/src/helpers/AudioBindingsWatchdog.ts +10 -7
- package/src/helpers/BlockedAudioTracker.ts +74 -0
- package/src/helpers/DynascaleManager.ts +46 -337
- package/src/helpers/MediaPlaybackWatchdog.ts +121 -0
- package/src/helpers/TrackSubscriptionManager.ts +243 -0
- package/src/helpers/ViewportTracker.ts +74 -19
- package/src/helpers/__tests__/BlockedAudioTracker.test.ts +114 -0
- package/src/helpers/__tests__/DynascaleManager.test.ts +175 -122
- package/src/helpers/__tests__/MediaPlaybackWatchdog.test.ts +180 -0
- package/src/helpers/__tests__/TrackSubscriptionManager.test.ts +310 -0
- package/src/helpers/__tests__/ViewportTracker.test.ts +83 -0
- package/src/helpers/__tests__/browsers.test.ts +85 -1
- package/src/helpers/browsers.ts +24 -0
- package/src/helpers/concurrency.ts +9 -10
- package/src/rtc/Publisher.ts +47 -1
- package/src/rtc/Subscriber.ts +42 -14
- package/src/rtc/__tests__/Publisher.test.ts +122 -10
- package/src/rtc/__tests__/Subscriber.test.ts +146 -1
- package/src/rtc/__tests__/mocks/webrtc.mocks.ts +13 -2
- package/src/rtc/helpers/__tests__/degradationPreference.test.ts +23 -0
- package/src/rtc/helpers/degradationPreference.ts +22 -0
- package/src/stats/rtc/types.ts +1 -0
- package/src/store/__tests__/rxUtils.test.ts +276 -0
- package/src/store/rxUtils.ts +19 -0
- package/src/types.ts +19 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { BehaviorSubject, Subject, throwError } from 'rxjs';
|
|
3
|
+
import { promiseWithResolvers } from '../../helpers/promise';
|
|
4
|
+
import {
|
|
5
|
+
createSafeAsyncSubscription,
|
|
6
|
+
createSubscription,
|
|
7
|
+
getCurrentValue,
|
|
8
|
+
setCurrentValue,
|
|
9
|
+
setCurrentValueAsync,
|
|
10
|
+
updateValue,
|
|
11
|
+
} from '../rxUtils';
|
|
12
|
+
|
|
13
|
+
describe('getCurrentValue', () => {
|
|
14
|
+
it('returns the current value of a BehaviorSubject', () => {
|
|
15
|
+
const subject = new BehaviorSubject(42);
|
|
16
|
+
expect(getCurrentValue(subject)).toBe(42);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('reflects subsequent emissions', () => {
|
|
20
|
+
const subject = new BehaviorSubject('a');
|
|
21
|
+
subject.next('b');
|
|
22
|
+
expect(getCurrentValue(subject)).toBe('b');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('rethrows errors emitted by the observable', () => {
|
|
26
|
+
const err = new Error('observable failed');
|
|
27
|
+
expect(() => getCurrentValue(throwError(() => err))).toThrow(err);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('setCurrentValue', () => {
|
|
32
|
+
it('sets a plain value and returns it', () => {
|
|
33
|
+
const subject = new BehaviorSubject(1);
|
|
34
|
+
const result = setCurrentValue(subject, 5);
|
|
35
|
+
expect(result).toBe(5);
|
|
36
|
+
expect(subject.getValue()).toBe(5);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('applies a function patch using the current value', () => {
|
|
40
|
+
const subject = new BehaviorSubject(10);
|
|
41
|
+
const result = setCurrentValue(subject, (n) => n * 2);
|
|
42
|
+
expect(result).toBe(20);
|
|
43
|
+
expect(subject.getValue()).toBe(20);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('emits the new value to subscribers', () => {
|
|
47
|
+
const subject = new BehaviorSubject(0);
|
|
48
|
+
const seen: number[] = [];
|
|
49
|
+
const sub = subject.subscribe((v) => seen.push(v));
|
|
50
|
+
setCurrentValue(subject, 1);
|
|
51
|
+
setCurrentValue(subject, (n) => n + 1);
|
|
52
|
+
sub.unsubscribe();
|
|
53
|
+
expect(seen).toEqual([0, 1, 2]);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('setCurrentValueAsync', () => {
|
|
58
|
+
it('passes the current value to the update fn and emits the resolved value', async () => {
|
|
59
|
+
const subject = new BehaviorSubject(1);
|
|
60
|
+
const update = vi.fn(async (n: number) => n + 1);
|
|
61
|
+
|
|
62
|
+
const result = await setCurrentValueAsync(subject, update);
|
|
63
|
+
|
|
64
|
+
expect(update).toHaveBeenCalledWith(1);
|
|
65
|
+
expect(result).toBe(2);
|
|
66
|
+
expect(subject.getValue()).toBe(2);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('serializes concurrent calls on the same subject so each sees the previous result', async () => {
|
|
70
|
+
const subject = new BehaviorSubject(0);
|
|
71
|
+
const observed: number[] = [];
|
|
72
|
+
|
|
73
|
+
const append = (delay: number) =>
|
|
74
|
+
setCurrentValueAsync(subject, async (n) => {
|
|
75
|
+
observed.push(n);
|
|
76
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
77
|
+
return n + 1;
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const [a, b, c] = await Promise.all([append(10), append(0), append(0)]);
|
|
81
|
+
|
|
82
|
+
expect(observed).toEqual([0, 1, 2]);
|
|
83
|
+
expect([a, b, c]).toEqual([1, 2, 3]);
|
|
84
|
+
expect(subject.getValue()).toBe(3);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('does not block updates on a different subject', async () => {
|
|
88
|
+
const a = new BehaviorSubject('a-0');
|
|
89
|
+
const b = new BehaviorSubject('b-0');
|
|
90
|
+
|
|
91
|
+
const gate = promiseWithResolvers();
|
|
92
|
+
|
|
93
|
+
const aPending = setCurrentValueAsync(a, async (v) => {
|
|
94
|
+
await gate.promise;
|
|
95
|
+
return `${v}-done`;
|
|
96
|
+
});
|
|
97
|
+
const bDone = await setCurrentValueAsync(b, async (v) => `${v}-done`);
|
|
98
|
+
|
|
99
|
+
expect(bDone).toBe('b-0-done');
|
|
100
|
+
expect(b.getValue()).toBe('b-0-done');
|
|
101
|
+
|
|
102
|
+
gate.resolve();
|
|
103
|
+
await expect(aPending).resolves.toBe('a-0-done');
|
|
104
|
+
expect(a.getValue()).toBe('a-0-done');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('propagates rejections without emitting and keeps the prior value', async () => {
|
|
108
|
+
const subject = new BehaviorSubject(7);
|
|
109
|
+
const emitted: number[] = [];
|
|
110
|
+
const sub = subject.subscribe((v) => emitted.push(v));
|
|
111
|
+
|
|
112
|
+
const boom = new Error('boom');
|
|
113
|
+
await expect(
|
|
114
|
+
setCurrentValueAsync(subject, async () => {
|
|
115
|
+
throw boom;
|
|
116
|
+
}),
|
|
117
|
+
).rejects.toBe(boom);
|
|
118
|
+
|
|
119
|
+
expect(subject.getValue()).toBe(7);
|
|
120
|
+
// Only the initial replay from the BehaviorSubject, no second emission.
|
|
121
|
+
expect(emitted).toEqual([7]);
|
|
122
|
+
sub.unsubscribe();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('continues to process queued updates after a rejection', async () => {
|
|
126
|
+
const subject = new BehaviorSubject(0);
|
|
127
|
+
|
|
128
|
+
const failing = setCurrentValueAsync(subject, async () => {
|
|
129
|
+
throw new Error('nope');
|
|
130
|
+
});
|
|
131
|
+
const succeeding = setCurrentValueAsync(subject, async (n) => n + 5);
|
|
132
|
+
|
|
133
|
+
await expect(failing).rejects.toThrow('nope');
|
|
134
|
+
await expect(succeeding).resolves.toBe(5);
|
|
135
|
+
expect(subject.getValue()).toBe(5);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('updateValue', () => {
|
|
140
|
+
it('returns the previous and new values', () => {
|
|
141
|
+
const subject = new BehaviorSubject(1);
|
|
142
|
+
const { lastValue, value } = updateValue(subject, 2);
|
|
143
|
+
expect(lastValue).toBe(1);
|
|
144
|
+
expect(value).toBe(2);
|
|
145
|
+
expect(subject.getValue()).toBe(2);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('rollback restores the previous value', () => {
|
|
149
|
+
const subject = new BehaviorSubject({ count: 3 });
|
|
150
|
+
const prior = subject.getValue();
|
|
151
|
+
|
|
152
|
+
const { rollback } = updateValue(subject, { count: 99 });
|
|
153
|
+
expect(subject.getValue()).toEqual({ count: 99 });
|
|
154
|
+
|
|
155
|
+
rollback();
|
|
156
|
+
expect(subject.getValue()).toBe(prior);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('accepts a function patch', () => {
|
|
160
|
+
const subject = new BehaviorSubject(10);
|
|
161
|
+
const { value } = updateValue(subject, (n) => n + 5);
|
|
162
|
+
expect(value).toBe(15);
|
|
163
|
+
expect(subject.getValue()).toBe(15);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe('createSubscription', () => {
|
|
168
|
+
it('invokes the handler with every emitted value', () => {
|
|
169
|
+
const subject = new Subject<number>();
|
|
170
|
+
const handler = vi.fn();
|
|
171
|
+
const unsubscribe = createSubscription(subject, handler);
|
|
172
|
+
|
|
173
|
+
subject.next(1);
|
|
174
|
+
subject.next(2);
|
|
175
|
+
unsubscribe();
|
|
176
|
+
|
|
177
|
+
expect(handler).toHaveBeenCalledTimes(2);
|
|
178
|
+
expect(handler).toHaveBeenNthCalledWith(1, 1);
|
|
179
|
+
expect(handler).toHaveBeenNthCalledWith(2, 2);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('stops receiving values after unsubscribe is called', () => {
|
|
183
|
+
const subject = new Subject<number>();
|
|
184
|
+
const handler = vi.fn();
|
|
185
|
+
const unsubscribe = createSubscription(subject, handler);
|
|
186
|
+
|
|
187
|
+
subject.next(1);
|
|
188
|
+
unsubscribe();
|
|
189
|
+
subject.next(2);
|
|
190
|
+
|
|
191
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
192
|
+
expect(handler).toHaveBeenCalledWith(1);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('routes errors to the provided onError handler', () => {
|
|
196
|
+
const err = new Error('observable failed');
|
|
197
|
+
const onError = vi.fn();
|
|
198
|
+
createSubscription(
|
|
199
|
+
throwError(() => err),
|
|
200
|
+
vi.fn(),
|
|
201
|
+
onError,
|
|
202
|
+
);
|
|
203
|
+
expect(onError).toHaveBeenCalledWith(err);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('swallows errors via the default onError when none is provided', () => {
|
|
207
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
208
|
+
|
|
209
|
+
expect(() =>
|
|
210
|
+
createSubscription(
|
|
211
|
+
throwError(() => new Error('boom')),
|
|
212
|
+
vi.fn(),
|
|
213
|
+
),
|
|
214
|
+
).not.toThrow();
|
|
215
|
+
expect(warn).toHaveBeenCalled();
|
|
216
|
+
|
|
217
|
+
warn.mockRestore();
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
describe('createSafeAsyncSubscription', () => {
|
|
222
|
+
it('runs the async handler for each emission', async () => {
|
|
223
|
+
const subject = new Subject<number>();
|
|
224
|
+
const handler = vi.fn(async () => {});
|
|
225
|
+
const unsubscribe = createSafeAsyncSubscription(subject, handler);
|
|
226
|
+
|
|
227
|
+
subject.next(1);
|
|
228
|
+
subject.next(2);
|
|
229
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
230
|
+
|
|
231
|
+
expect(handler).toHaveBeenCalledTimes(2);
|
|
232
|
+
expect(handler).toHaveBeenNthCalledWith(1, 1);
|
|
233
|
+
expect(handler).toHaveBeenNthCalledWith(2, 2);
|
|
234
|
+
unsubscribe();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('serializes handlers so a slow one blocks the next', async () => {
|
|
238
|
+
const subject = new Subject<number>();
|
|
239
|
+
const events: string[] = [];
|
|
240
|
+
const gate = promiseWithResolvers();
|
|
241
|
+
|
|
242
|
+
const unsubscribe = createSafeAsyncSubscription(subject, async (v) => {
|
|
243
|
+
events.push(`start:${v}`);
|
|
244
|
+
if (v === 1) await gate.promise;
|
|
245
|
+
events.push(`end:${v}`);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
subject.next(1);
|
|
249
|
+
subject.next(2);
|
|
250
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
251
|
+
|
|
252
|
+
// Second handler hasn't started yet because the first is still in-flight.
|
|
253
|
+
expect(events).toEqual(['start:1']);
|
|
254
|
+
|
|
255
|
+
gate.resolve();
|
|
256
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
257
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
258
|
+
|
|
259
|
+
expect(events).toEqual(['start:1', 'end:1', 'start:2', 'end:2']);
|
|
260
|
+
unsubscribe();
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('stops invoking the handler after unsubscribe', async () => {
|
|
264
|
+
const subject = new Subject<number>();
|
|
265
|
+
const handler = vi.fn(async () => {});
|
|
266
|
+
const unsubscribe = createSafeAsyncSubscription(subject, handler);
|
|
267
|
+
|
|
268
|
+
subject.next(1);
|
|
269
|
+
unsubscribe();
|
|
270
|
+
subject.next(2);
|
|
271
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
272
|
+
|
|
273
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
274
|
+
expect(handler).toHaveBeenCalledWith(1);
|
|
275
|
+
});
|
|
276
|
+
});
|
package/src/store/rxUtils.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { withoutConcurrency } from '../helpers/concurrency';
|
|
|
3
3
|
import { videoLoggerSystem } from '../logger';
|
|
4
4
|
|
|
5
5
|
type FunctionPatch<T> = (currentValue: T) => T;
|
|
6
|
+
type AsyncFunctionPatch<T> = (currentValue: T) => Promise<T>;
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* A value or a function which takes the current value and returns a new value.
|
|
@@ -59,6 +60,24 @@ export const setCurrentValue = <T>(subject: Subject<T>, update: Patch<T>) => {
|
|
|
59
60
|
return next;
|
|
60
61
|
};
|
|
61
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Updates the value of the provided Subject asynchronously.
|
|
65
|
+
* Locks the subject to prevent concurrent updates.
|
|
66
|
+
*
|
|
67
|
+
* @param subject the subject to update.
|
|
68
|
+
* @param update the update to apply to the subject.
|
|
69
|
+
*/
|
|
70
|
+
export const setCurrentValueAsync = async <T>(
|
|
71
|
+
subject: Subject<T>,
|
|
72
|
+
update: AsyncFunctionPatch<T>,
|
|
73
|
+
) => {
|
|
74
|
+
return withoutConcurrency(subject, async () => {
|
|
75
|
+
const next = await update(getCurrentValue(subject));
|
|
76
|
+
subject.next(next);
|
|
77
|
+
return next;
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
|
|
62
81
|
/**
|
|
63
82
|
* Updates the value of the provided Subject and returns the previous value
|
|
64
83
|
* and a function to roll back the update.
|
package/src/types.ts
CHANGED
|
@@ -90,6 +90,25 @@ export interface StreamVideoParticipant extends Participant {
|
|
|
90
90
|
*/
|
|
91
91
|
pausedTracks?: TrackType[];
|
|
92
92
|
|
|
93
|
+
/**
|
|
94
|
+
* The list of tracks that are currently not producing media.
|
|
95
|
+
*
|
|
96
|
+
* For remote participants this is currently surfaced for `TrackType.AUDIO`
|
|
97
|
+
* only and reflects the receiver-side `RTCRtpReceiver` track `mute`/`unmute`
|
|
98
|
+
* state, so it covers system mute on the sender (OS audio session
|
|
99
|
+
* interruption, etc.), the sender pausing its track, sustained RTP stalls,
|
|
100
|
+
* and SFU drops. Remote video and screen-share interruption is not tracked.
|
|
101
|
+
*
|
|
102
|
+
* For the local participant it reflects the local track `mute`/`unmute`
|
|
103
|
+
* events surfaced by the browser (e.g. bluetooth disconnect, OS-level
|
|
104
|
+
* mic/camera kill switch, iOS audio session interruption).
|
|
105
|
+
*
|
|
106
|
+
* Orthogonal to `publishedTracks`: a track can be in `publishedTracks`
|
|
107
|
+
* AND in `interruptedTracks` (the participant intends to publish, but
|
|
108
|
+
* no media is flowing right now).
|
|
109
|
+
*/
|
|
110
|
+
interruptedTracks?: TrackType[];
|
|
111
|
+
|
|
93
112
|
/**
|
|
94
113
|
* True if the participant is the local participant.
|
|
95
114
|
*/
|