@stream-io/video-client 1.7.4 → 1.8.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 +7 -0
- package/dist/index.browser.es.js +174 -91
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +173 -90
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +174 -91
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +18 -14
- package/dist/src/helpers/DynascaleManager.d.ts +41 -4
- package/dist/src/store/CallState.d.ts +11 -1
- package/package.json +1 -1
- package/src/Call.ts +45 -121
- package/src/helpers/DynascaleManager.ts +176 -20
- package/src/helpers/__tests__/DynascaleManager.test.ts +79 -112
- package/src/store/CallState.ts +43 -3
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { Call } from '../Call';
|
|
2
1
|
import {
|
|
3
2
|
AudioTrackType,
|
|
4
3
|
DebounceType,
|
|
@@ -6,8 +5,9 @@ import {
|
|
|
6
5
|
VideoTrackType,
|
|
7
6
|
VisibilityState,
|
|
8
7
|
} from '../types';
|
|
9
|
-
import { VideoDimension } from '../gen/video/sfu/models/models';
|
|
8
|
+
import { TrackType, VideoDimension } from '../gen/video/sfu/models/models';
|
|
10
9
|
import {
|
|
10
|
+
BehaviorSubject,
|
|
11
11
|
combineLatest,
|
|
12
12
|
distinctUntilChanged,
|
|
13
13
|
distinctUntilKeyChanged,
|
|
@@ -18,7 +18,16 @@ import {
|
|
|
18
18
|
import { ViewportTracker } from './ViewportTracker';
|
|
19
19
|
import { getLogger } from '../logger';
|
|
20
20
|
import { isFirefox, isSafari } from './browsers';
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
hasScreenShare,
|
|
23
|
+
hasScreenShareAudio,
|
|
24
|
+
hasVideo,
|
|
25
|
+
} from './participantUtils';
|
|
26
|
+
import type { TrackSubscriptionDetails } from '../gen/video/sfu/signal_rpc/signal';
|
|
27
|
+
import type { CallState } from '../store';
|
|
28
|
+
import type { StreamSfuClient } from '../StreamSfuClient';
|
|
29
|
+
import { SpeakerManager } from '../devices';
|
|
30
|
+
import { getCurrentValue, setCurrentValue } from '../store/rxUtils';
|
|
22
31
|
|
|
23
32
|
const DEFAULT_VIEWPORT_VISIBILITY_STATE: Record<
|
|
24
33
|
VideoTrackType,
|
|
@@ -28,6 +37,20 @@ const DEFAULT_VIEWPORT_VISIBILITY_STATE: Record<
|
|
|
28
37
|
screenShareTrack: VisibilityState.UNKNOWN,
|
|
29
38
|
} as const;
|
|
30
39
|
|
|
40
|
+
type VideoTrackSubscriptionOverride =
|
|
41
|
+
| {
|
|
42
|
+
enabled: true;
|
|
43
|
+
dimension: VideoDimension;
|
|
44
|
+
}
|
|
45
|
+
| { enabled: false };
|
|
46
|
+
|
|
47
|
+
const globalOverrideKey = Symbol('globalOverrideKey');
|
|
48
|
+
|
|
49
|
+
interface VideoTrackSubscriptionOverrides {
|
|
50
|
+
[sessionId: string]: VideoTrackSubscriptionOverride | undefined;
|
|
51
|
+
[globalOverrideKey]?: VideoTrackSubscriptionOverride;
|
|
52
|
+
}
|
|
53
|
+
|
|
31
54
|
/**
|
|
32
55
|
* A manager class that handles dynascale related tasks like:
|
|
33
56
|
*
|
|
@@ -45,17 +68,151 @@ export class DynascaleManager {
|
|
|
45
68
|
readonly viewportTracker = new ViewportTracker();
|
|
46
69
|
|
|
47
70
|
private logger = getLogger(['DynascaleManager']);
|
|
48
|
-
private
|
|
71
|
+
private callState: CallState;
|
|
72
|
+
private speaker: SpeakerManager;
|
|
73
|
+
private sfuClient: StreamSfuClient | undefined;
|
|
74
|
+
private pendingSubscriptionsUpdate: NodeJS.Timeout | null = null;
|
|
75
|
+
|
|
76
|
+
private videoTrackSubscriptionOverridesSubject =
|
|
77
|
+
new BehaviorSubject<VideoTrackSubscriptionOverrides>({});
|
|
78
|
+
|
|
79
|
+
videoTrackSubscriptionOverrides$ =
|
|
80
|
+
this.videoTrackSubscriptionOverridesSubject.asObservable();
|
|
81
|
+
|
|
82
|
+
incomingVideoSettings$ = this.videoTrackSubscriptionOverrides$.pipe(
|
|
83
|
+
map((overrides) => {
|
|
84
|
+
const { [globalOverrideKey]: globalSettings, ...participants } =
|
|
85
|
+
overrides;
|
|
86
|
+
return {
|
|
87
|
+
enabled: globalSettings?.enabled !== false,
|
|
88
|
+
preferredResolution: globalSettings?.enabled
|
|
89
|
+
? globalSettings.dimension
|
|
90
|
+
: undefined,
|
|
91
|
+
participants: Object.fromEntries(
|
|
92
|
+
Object.entries(participants).map(
|
|
93
|
+
([sessionId, participantOverride]) => [
|
|
94
|
+
sessionId,
|
|
95
|
+
{
|
|
96
|
+
enabled: participantOverride?.enabled !== false,
|
|
97
|
+
preferredResolution: participantOverride?.enabled
|
|
98
|
+
? participantOverride.dimension
|
|
99
|
+
: undefined,
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
),
|
|
103
|
+
),
|
|
104
|
+
isParticipantVideoEnabled: (sessionId: string) =>
|
|
105
|
+
overrides[sessionId]?.enabled ??
|
|
106
|
+
overrides[globalOverrideKey]?.enabled ??
|
|
107
|
+
true,
|
|
108
|
+
};
|
|
109
|
+
}),
|
|
110
|
+
shareReplay(1),
|
|
111
|
+
);
|
|
49
112
|
|
|
50
113
|
/**
|
|
51
114
|
* Creates a new DynascaleManager instance.
|
|
52
115
|
*
|
|
53
116
|
* @param call the call to manage.
|
|
54
117
|
*/
|
|
55
|
-
constructor(
|
|
56
|
-
this.
|
|
118
|
+
constructor(callState: CallState, speaker: SpeakerManager) {
|
|
119
|
+
this.callState = callState;
|
|
120
|
+
this.speaker = speaker;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
setSfuClient(sfuClient: StreamSfuClient | undefined) {
|
|
124
|
+
this.sfuClient = sfuClient;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
get trackSubscriptions() {
|
|
128
|
+
const subscriptions: TrackSubscriptionDetails[] = [];
|
|
129
|
+
for (const p of this.callState.remoteParticipants) {
|
|
130
|
+
// NOTE: audio tracks don't have to be requested explicitly
|
|
131
|
+
// as the SFU will implicitly subscribe us to all of them,
|
|
132
|
+
// once they become available.
|
|
133
|
+
if (p.videoDimension && hasVideo(p)) {
|
|
134
|
+
const override =
|
|
135
|
+
this.videoTrackSubscriptionOverrides[p.sessionId] ??
|
|
136
|
+
this.videoTrackSubscriptionOverrides[globalOverrideKey];
|
|
137
|
+
|
|
138
|
+
if (override?.enabled !== false) {
|
|
139
|
+
subscriptions.push({
|
|
140
|
+
userId: p.userId,
|
|
141
|
+
sessionId: p.sessionId,
|
|
142
|
+
trackType: TrackType.VIDEO,
|
|
143
|
+
dimension: override?.dimension ?? p.videoDimension,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (p.screenShareDimension && hasScreenShare(p)) {
|
|
148
|
+
subscriptions.push({
|
|
149
|
+
userId: p.userId,
|
|
150
|
+
sessionId: p.sessionId,
|
|
151
|
+
trackType: TrackType.SCREEN_SHARE,
|
|
152
|
+
dimension: p.screenShareDimension,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
if (hasScreenShareAudio(p)) {
|
|
156
|
+
subscriptions.push({
|
|
157
|
+
userId: p.userId,
|
|
158
|
+
sessionId: p.sessionId,
|
|
159
|
+
trackType: TrackType.SCREEN_SHARE_AUDIO,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return subscriptions;
|
|
57
164
|
}
|
|
58
165
|
|
|
166
|
+
get videoTrackSubscriptionOverrides() {
|
|
167
|
+
return getCurrentValue(this.videoTrackSubscriptionOverrides$);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
setVideoTrackSubscriptionOverrides = (
|
|
171
|
+
override: VideoTrackSubscriptionOverride | undefined,
|
|
172
|
+
sessionIds?: string[],
|
|
173
|
+
) => {
|
|
174
|
+
if (!sessionIds) {
|
|
175
|
+
return setCurrentValue(
|
|
176
|
+
this.videoTrackSubscriptionOverridesSubject,
|
|
177
|
+
override ? { [globalOverrideKey]: override } : {},
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return setCurrentValue(
|
|
182
|
+
this.videoTrackSubscriptionOverridesSubject,
|
|
183
|
+
(overrides) => ({
|
|
184
|
+
...overrides,
|
|
185
|
+
...Object.fromEntries(sessionIds.map((id) => [id, override])),
|
|
186
|
+
}),
|
|
187
|
+
);
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
applyTrackSubscriptions = (
|
|
191
|
+
debounceType: DebounceType = DebounceType.SLOW,
|
|
192
|
+
) => {
|
|
193
|
+
if (this.pendingSubscriptionsUpdate) {
|
|
194
|
+
clearTimeout(this.pendingSubscriptionsUpdate);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const updateSubscriptions = () => {
|
|
198
|
+
this.pendingSubscriptionsUpdate = null;
|
|
199
|
+
this.sfuClient
|
|
200
|
+
?.updateSubscriptions(this.trackSubscriptions)
|
|
201
|
+
.catch((err: unknown) => {
|
|
202
|
+
this.logger('debug', `Failed to update track subscriptions`, err);
|
|
203
|
+
});
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
if (debounceType) {
|
|
207
|
+
this.pendingSubscriptionsUpdate = setTimeout(
|
|
208
|
+
updateSubscriptions,
|
|
209
|
+
debounceType,
|
|
210
|
+
);
|
|
211
|
+
} else {
|
|
212
|
+
updateSubscriptions();
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
59
216
|
/**
|
|
60
217
|
* Will begin tracking the given element for visibility changes within the
|
|
61
218
|
* configured viewport element (`call.setViewport`).
|
|
@@ -71,7 +228,7 @@ export class DynascaleManager {
|
|
|
71
228
|
trackType: VideoTrackType,
|
|
72
229
|
) => {
|
|
73
230
|
const cleanup = this.viewportTracker.observe(element, (entry) => {
|
|
74
|
-
this.
|
|
231
|
+
this.callState.updateParticipant(sessionId, (participant) => {
|
|
75
232
|
const previousVisibilityState =
|
|
76
233
|
participant.viewportVisibilityState ??
|
|
77
234
|
DEFAULT_VIEWPORT_VISIBILITY_STATE;
|
|
@@ -97,7 +254,7 @@ export class DynascaleManager {
|
|
|
97
254
|
// reset visibility state to UNKNOWN upon cleanup
|
|
98
255
|
// so that the layouts that are not actively observed
|
|
99
256
|
// can still function normally (runtime layout switching)
|
|
100
|
-
this.
|
|
257
|
+
this.callState.updateParticipant(sessionId, (participant) => {
|
|
101
258
|
const previousVisibilityState =
|
|
102
259
|
participant.viewportVisibilityState ??
|
|
103
260
|
DEFAULT_VIEWPORT_VISIBILITY_STATE;
|
|
@@ -142,7 +299,7 @@ export class DynascaleManager {
|
|
|
142
299
|
trackType: VideoTrackType,
|
|
143
300
|
) => {
|
|
144
301
|
const boundParticipant =
|
|
145
|
-
this.
|
|
302
|
+
this.callState.findParticipantBySessionId(sessionId);
|
|
146
303
|
if (!boundParticipant) return;
|
|
147
304
|
|
|
148
305
|
const requestTrackWithDimensions = (
|
|
@@ -157,14 +314,13 @@ export class DynascaleManager {
|
|
|
157
314
|
this.logger('debug', `Ignoring 0x0 dimension`, boundParticipant);
|
|
158
315
|
dimension = undefined;
|
|
159
316
|
}
|
|
160
|
-
this.
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
);
|
|
317
|
+
this.callState.updateParticipantTracks(trackType, {
|
|
318
|
+
[sessionId]: { dimension },
|
|
319
|
+
});
|
|
320
|
+
this.applyTrackSubscriptions(debounceType);
|
|
165
321
|
};
|
|
166
322
|
|
|
167
|
-
const participant$ = this.
|
|
323
|
+
const participant$ = this.callState.participants$.pipe(
|
|
168
324
|
map(
|
|
169
325
|
(participants) =>
|
|
170
326
|
participants.find(
|
|
@@ -324,10 +480,10 @@ export class DynascaleManager {
|
|
|
324
480
|
sessionId: string,
|
|
325
481
|
trackType: AudioTrackType,
|
|
326
482
|
) => {
|
|
327
|
-
const participant = this.
|
|
483
|
+
const participant = this.callState.findParticipantBySessionId(sessionId);
|
|
328
484
|
if (!participant || participant.isLocalParticipant) return;
|
|
329
485
|
|
|
330
|
-
const participant$ = this.
|
|
486
|
+
const participant$ = this.callState.participants$.pipe(
|
|
331
487
|
map(
|
|
332
488
|
(participants) =>
|
|
333
489
|
participants.find(
|
|
@@ -364,7 +520,7 @@ export class DynascaleManager {
|
|
|
364
520
|
// audio output device shall be set after the audio element is played
|
|
365
521
|
// otherwise, the browser will not pick it up, and will always
|
|
366
522
|
// play audio through the system's default device
|
|
367
|
-
const { selectedDevice } = this.
|
|
523
|
+
const { selectedDevice } = this.speaker.state;
|
|
368
524
|
if (selectedDevice && 'setSinkId' in audioElement) {
|
|
369
525
|
audioElement.setSinkId(selectedDevice);
|
|
370
526
|
}
|
|
@@ -374,14 +530,14 @@ export class DynascaleManager {
|
|
|
374
530
|
|
|
375
531
|
const sinkIdSubscription = !('setSinkId' in audioElement)
|
|
376
532
|
? null
|
|
377
|
-
: this.
|
|
533
|
+
: this.speaker.state.selectedDevice$.subscribe((deviceId) => {
|
|
378
534
|
if (deviceId) {
|
|
379
535
|
audioElement.setSinkId(deviceId);
|
|
380
536
|
}
|
|
381
537
|
});
|
|
382
538
|
|
|
383
539
|
const volumeSubscription = combineLatest([
|
|
384
|
-
this.
|
|
540
|
+
this.speaker.state.volume$,
|
|
385
541
|
participant$.pipe(distinctUntilKeyChanged('audioVolume')),
|
|
386
542
|
]).subscribe(([volume, p]) => {
|
|
387
543
|
audioElement.volume = p.audioVolume ?? volume;
|
|
@@ -9,7 +9,7 @@ import { DynascaleManager } from '../DynascaleManager';
|
|
|
9
9
|
import { Call } from '../../Call';
|
|
10
10
|
import { StreamClient } from '../../coordinator/connection/client';
|
|
11
11
|
import { StreamVideoWriteableStateStore } from '../../store';
|
|
12
|
-
import {
|
|
12
|
+
import { VisibilityState } from '../../types';
|
|
13
13
|
import { noopComparator } from '../../sorting';
|
|
14
14
|
import { TrackType } from '../../gen/video/sfu/models/models';
|
|
15
15
|
|
|
@@ -25,7 +25,7 @@ describe('DynascaleManager', () => {
|
|
|
25
25
|
clientStore: new StreamVideoWriteableStateStore(),
|
|
26
26
|
});
|
|
27
27
|
call.setSortParticipantsBy(noopComparator());
|
|
28
|
-
dynascaleManager = new DynascaleManager(call);
|
|
28
|
+
dynascaleManager = new DynascaleManager(call.state, call.speaker);
|
|
29
29
|
});
|
|
30
30
|
|
|
31
31
|
afterEach(() => {
|
|
@@ -194,7 +194,10 @@ describe('DynascaleManager', () => {
|
|
|
194
194
|
});
|
|
195
195
|
|
|
196
196
|
it('video: should update subscription when track becomes available', () => {
|
|
197
|
-
const updateSubscription = vi.spyOn(
|
|
197
|
+
const updateSubscription = vi.spyOn(
|
|
198
|
+
call.state,
|
|
199
|
+
'updateParticipantTracks',
|
|
200
|
+
);
|
|
198
201
|
|
|
199
202
|
// @ts-ignore
|
|
200
203
|
call.state.updateOrAddParticipant('session-id', {
|
|
@@ -213,52 +216,45 @@ describe('DynascaleManager', () => {
|
|
|
213
216
|
expect(videoElement.muted).toBe(true);
|
|
214
217
|
expect(videoElement.playsInline).toBe(true);
|
|
215
218
|
|
|
216
|
-
expect(updateSubscription).toHaveBeenCalledWith(
|
|
217
|
-
'
|
|
218
|
-
|
|
219
|
-
DebounceType.FAST,
|
|
220
|
-
);
|
|
219
|
+
expect(updateSubscription).toHaveBeenCalledWith('videoTrack', {
|
|
220
|
+
'session-id': { dimension: undefined },
|
|
221
|
+
});
|
|
221
222
|
|
|
222
223
|
call.state.updateParticipant('session-id', {
|
|
223
224
|
publishedTracks: [TrackType.VIDEO],
|
|
224
225
|
});
|
|
225
226
|
|
|
226
|
-
expect(updateSubscription).toHaveBeenCalledWith(
|
|
227
|
-
'
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
width: videoElement.clientWidth,
|
|
232
|
-
height: videoElement.clientHeight,
|
|
233
|
-
},
|
|
227
|
+
expect(updateSubscription).toHaveBeenCalledWith('videoTrack', {
|
|
228
|
+
'session-id': {
|
|
229
|
+
dimension: {
|
|
230
|
+
width: videoElement.clientWidth,
|
|
231
|
+
height: videoElement.clientHeight,
|
|
234
232
|
},
|
|
235
233
|
},
|
|
236
|
-
|
|
237
|
-
);
|
|
234
|
+
});
|
|
238
235
|
|
|
239
236
|
call.state.updateParticipant('session-id', {
|
|
240
237
|
publishedTracks: [TrackType.VIDEO],
|
|
241
238
|
});
|
|
242
239
|
|
|
243
|
-
expect(updateSubscription).toHaveBeenCalledWith(
|
|
244
|
-
'
|
|
245
|
-
|
|
246
|
-
DebounceType.FAST,
|
|
247
|
-
);
|
|
240
|
+
expect(updateSubscription).toHaveBeenCalledWith('videoTrack', {
|
|
241
|
+
'session-id': { dimension: undefined },
|
|
242
|
+
});
|
|
248
243
|
|
|
249
244
|
cleanup?.();
|
|
250
245
|
|
|
251
|
-
expect(updateSubscription).toHaveBeenLastCalledWith(
|
|
252
|
-
'
|
|
253
|
-
|
|
254
|
-
DebounceType.FAST,
|
|
255
|
-
);
|
|
246
|
+
expect(updateSubscription).toHaveBeenLastCalledWith('videoTrack', {
|
|
247
|
+
'session-id': { dimension: undefined },
|
|
248
|
+
});
|
|
256
249
|
});
|
|
257
250
|
|
|
258
251
|
it('video: should play video when track becomes available', () => {
|
|
259
252
|
vi.useFakeTimers();
|
|
260
253
|
|
|
261
|
-
const updateSubscription = vi.spyOn(
|
|
254
|
+
const updateSubscription = vi.spyOn(
|
|
255
|
+
call.state,
|
|
256
|
+
'updateParticipantTracks',
|
|
257
|
+
);
|
|
262
258
|
const play = vi.spyOn(videoElement, 'play').mockResolvedValue();
|
|
263
259
|
|
|
264
260
|
// @ts-ignore
|
|
@@ -282,28 +278,22 @@ describe('DynascaleManager', () => {
|
|
|
282
278
|
|
|
283
279
|
vi.runAllTimers();
|
|
284
280
|
|
|
285
|
-
expect(updateSubscription).toHaveBeenCalledWith(
|
|
286
|
-
'
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
width: videoElement.clientWidth,
|
|
291
|
-
height: videoElement.clientHeight,
|
|
292
|
-
},
|
|
281
|
+
expect(updateSubscription).toHaveBeenCalledWith('videoTrack', {
|
|
282
|
+
'session-id': {
|
|
283
|
+
dimension: {
|
|
284
|
+
width: videoElement.clientWidth,
|
|
285
|
+
height: videoElement.clientHeight,
|
|
293
286
|
},
|
|
294
287
|
},
|
|
295
|
-
|
|
296
|
-
);
|
|
288
|
+
});
|
|
297
289
|
expect(play).toHaveBeenCalled();
|
|
298
290
|
expect(videoElement.srcObject).toBe(mediaStream);
|
|
299
291
|
|
|
300
292
|
cleanup?.();
|
|
301
293
|
|
|
302
|
-
expect(updateSubscription).toHaveBeenLastCalledWith(
|
|
303
|
-
'
|
|
304
|
-
|
|
305
|
-
DebounceType.FAST,
|
|
306
|
-
);
|
|
294
|
+
expect(updateSubscription).toHaveBeenLastCalledWith('videoTrack', {
|
|
295
|
+
'session-id': { dimension: undefined },
|
|
296
|
+
});
|
|
307
297
|
});
|
|
308
298
|
|
|
309
299
|
it('video: should update subscription when element becomes visible', () => {
|
|
@@ -318,7 +308,10 @@ describe('DynascaleManager', () => {
|
|
|
318
308
|
},
|
|
319
309
|
});
|
|
320
310
|
|
|
321
|
-
const updateSubscription = vi.spyOn(
|
|
311
|
+
const updateSubscription = vi.spyOn(
|
|
312
|
+
call.state,
|
|
313
|
+
'updateParticipantTracks',
|
|
314
|
+
);
|
|
322
315
|
|
|
323
316
|
const cleanup = dynascaleManager.bindVideoElement(
|
|
324
317
|
videoElement,
|
|
@@ -326,11 +319,9 @@ describe('DynascaleManager', () => {
|
|
|
326
319
|
'videoTrack',
|
|
327
320
|
);
|
|
328
321
|
|
|
329
|
-
expect(updateSubscription).toHaveBeenCalledWith(
|
|
330
|
-
'
|
|
331
|
-
|
|
332
|
-
DebounceType.FAST,
|
|
333
|
-
);
|
|
322
|
+
expect(updateSubscription).toHaveBeenCalledWith('videoTrack', {
|
|
323
|
+
'session-id': { dimension: undefined },
|
|
324
|
+
});
|
|
334
325
|
|
|
335
326
|
call.state.updateParticipant('session-id', {
|
|
336
327
|
viewportVisibilityState: {
|
|
@@ -339,18 +330,14 @@ describe('DynascaleManager', () => {
|
|
|
339
330
|
},
|
|
340
331
|
});
|
|
341
332
|
|
|
342
|
-
expect(updateSubscription).toHaveBeenCalledWith(
|
|
343
|
-
'
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
width: videoElement.clientWidth,
|
|
348
|
-
height: videoElement.clientHeight,
|
|
349
|
-
},
|
|
333
|
+
expect(updateSubscription).toHaveBeenCalledWith('videoTrack', {
|
|
334
|
+
'session-id': {
|
|
335
|
+
dimension: {
|
|
336
|
+
width: videoElement.clientWidth,
|
|
337
|
+
height: videoElement.clientHeight,
|
|
350
338
|
},
|
|
351
339
|
},
|
|
352
|
-
|
|
353
|
-
);
|
|
340
|
+
});
|
|
354
341
|
|
|
355
342
|
call.state.updateParticipant('session-id', {
|
|
356
343
|
viewportVisibilityState: {
|
|
@@ -359,11 +346,9 @@ describe('DynascaleManager', () => {
|
|
|
359
346
|
},
|
|
360
347
|
});
|
|
361
348
|
|
|
362
|
-
expect(updateSubscription).toHaveBeenCalledWith(
|
|
363
|
-
'
|
|
364
|
-
|
|
365
|
-
DebounceType.MEDIUM,
|
|
366
|
-
);
|
|
349
|
+
expect(updateSubscription).toHaveBeenCalledWith('videoTrack', {
|
|
350
|
+
'session-id': { dimension: undefined },
|
|
351
|
+
});
|
|
367
352
|
|
|
368
353
|
call.state.updateParticipant('session-id', {
|
|
369
354
|
viewportVisibilityState: {
|
|
@@ -372,26 +357,20 @@ describe('DynascaleManager', () => {
|
|
|
372
357
|
},
|
|
373
358
|
});
|
|
374
359
|
|
|
375
|
-
expect(updateSubscription).toHaveBeenCalledWith(
|
|
376
|
-
'
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
width: videoElement.clientWidth,
|
|
381
|
-
height: videoElement.clientHeight,
|
|
382
|
-
},
|
|
360
|
+
expect(updateSubscription).toHaveBeenCalledWith('videoTrack', {
|
|
361
|
+
'session-id': {
|
|
362
|
+
dimension: {
|
|
363
|
+
width: videoElement.clientWidth,
|
|
364
|
+
height: videoElement.clientHeight,
|
|
383
365
|
},
|
|
384
366
|
},
|
|
385
|
-
|
|
386
|
-
);
|
|
367
|
+
});
|
|
387
368
|
|
|
388
369
|
cleanup?.();
|
|
389
370
|
|
|
390
|
-
expect(updateSubscription).toHaveBeenLastCalledWith(
|
|
391
|
-
'
|
|
392
|
-
|
|
393
|
-
DebounceType.FAST,
|
|
394
|
-
);
|
|
371
|
+
expect(updateSubscription).toHaveBeenLastCalledWith('videoTrack', {
|
|
372
|
+
'session-id': { dimension: undefined },
|
|
373
|
+
});
|
|
395
374
|
});
|
|
396
375
|
|
|
397
376
|
it('video: should update subscription when element resizes', () => {
|
|
@@ -406,7 +385,7 @@ describe('DynascaleManager', () => {
|
|
|
406
385
|
},
|
|
407
386
|
});
|
|
408
387
|
|
|
409
|
-
let updateSubscription = vi.spyOn(call, '
|
|
388
|
+
let updateSubscription = vi.spyOn(call.state, 'updateParticipantTracks');
|
|
410
389
|
|
|
411
390
|
let resizeObserverCallback: ResizeObserverCallback;
|
|
412
391
|
window.ResizeObserver = class ResizeObserver {
|
|
@@ -428,18 +407,14 @@ describe('DynascaleManager', () => {
|
|
|
428
407
|
'videoTrack',
|
|
429
408
|
);
|
|
430
409
|
|
|
431
|
-
expect(updateSubscription).toHaveBeenCalledWith(
|
|
432
|
-
'
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
width: videoElement.clientWidth,
|
|
437
|
-
height: videoElement.clientHeight,
|
|
438
|
-
},
|
|
410
|
+
expect(updateSubscription).toHaveBeenCalledWith('videoTrack', {
|
|
411
|
+
'session-id': {
|
|
412
|
+
dimension: {
|
|
413
|
+
width: videoElement.clientWidth,
|
|
414
|
+
height: videoElement.clientHeight,
|
|
439
415
|
},
|
|
440
416
|
},
|
|
441
|
-
|
|
442
|
-
);
|
|
417
|
+
});
|
|
443
418
|
|
|
444
419
|
// @ts-ignore simulate resize
|
|
445
420
|
videoElement.clientHeight = 101;
|
|
@@ -449,19 +424,15 @@ describe('DynascaleManager', () => {
|
|
|
449
424
|
// @ts-ignore simulate resize
|
|
450
425
|
resizeObserverCallback();
|
|
451
426
|
|
|
452
|
-
expect(updateSubscription).toHaveBeenCalledWith(
|
|
453
|
-
'
|
|
454
|
-
|
|
455
|
-
DebounceType.SLOW,
|
|
456
|
-
);
|
|
427
|
+
expect(updateSubscription).toHaveBeenCalledWith('videoTrack', {
|
|
428
|
+
'session-id': { dimension: { width: 101, height: 101 } },
|
|
429
|
+
});
|
|
457
430
|
|
|
458
431
|
cleanup?.();
|
|
459
432
|
|
|
460
|
-
expect(updateSubscription).toHaveBeenLastCalledWith(
|
|
461
|
-
'
|
|
462
|
-
|
|
463
|
-
DebounceType.FAST,
|
|
464
|
-
);
|
|
433
|
+
expect(updateSubscription).toHaveBeenLastCalledWith('videoTrack', {
|
|
434
|
+
'session-id': { dimension: undefined },
|
|
435
|
+
});
|
|
465
436
|
});
|
|
466
437
|
|
|
467
438
|
it('video: should unsubscribe when element dimensions are zero', () => {
|
|
@@ -476,7 +447,7 @@ describe('DynascaleManager', () => {
|
|
|
476
447
|
},
|
|
477
448
|
});
|
|
478
449
|
|
|
479
|
-
let updateSubscription = vi.spyOn(call, '
|
|
450
|
+
let updateSubscription = vi.spyOn(call.state, 'updateParticipantTracks');
|
|
480
451
|
|
|
481
452
|
// @ts-ignore simulate resize
|
|
482
453
|
videoElement.clientHeight = 0;
|
|
@@ -489,19 +460,15 @@ describe('DynascaleManager', () => {
|
|
|
489
460
|
'videoTrack',
|
|
490
461
|
);
|
|
491
462
|
|
|
492
|
-
expect(updateSubscription).toHaveBeenCalledWith(
|
|
493
|
-
'
|
|
494
|
-
|
|
495
|
-
DebounceType.FAST,
|
|
496
|
-
);
|
|
463
|
+
expect(updateSubscription).toHaveBeenCalledWith('videoTrack', {
|
|
464
|
+
'session-id': { dimension: undefined },
|
|
465
|
+
});
|
|
497
466
|
|
|
498
467
|
cleanup?.();
|
|
499
468
|
|
|
500
|
-
expect(updateSubscription).toHaveBeenLastCalledWith(
|
|
501
|
-
'
|
|
502
|
-
|
|
503
|
-
DebounceType.FAST,
|
|
504
|
-
);
|
|
469
|
+
expect(updateSubscription).toHaveBeenLastCalledWith('videoTrack', {
|
|
470
|
+
'session-id': { dimension: undefined },
|
|
471
|
+
});
|
|
505
472
|
});
|
|
506
473
|
});
|
|
507
474
|
});
|
package/src/store/CallState.ts
CHANGED
|
@@ -9,9 +9,11 @@ import type { Patch } from './rxUtils';
|
|
|
9
9
|
import * as RxUtils from './rxUtils';
|
|
10
10
|
import { CallingState } from './CallingState';
|
|
11
11
|
import {
|
|
12
|
-
StreamVideoParticipant,
|
|
13
|
-
StreamVideoParticipantPatch,
|
|
14
|
-
StreamVideoParticipantPatches,
|
|
12
|
+
type StreamVideoParticipant,
|
|
13
|
+
type StreamVideoParticipantPatch,
|
|
14
|
+
type StreamVideoParticipantPatches,
|
|
15
|
+
type SubscriptionChanges,
|
|
16
|
+
VideoTrackType,
|
|
15
17
|
VisibilityState,
|
|
16
18
|
} from '../types';
|
|
17
19
|
import { CallStatsReport } from '../stats';
|
|
@@ -903,6 +905,44 @@ export class CallState {
|
|
|
903
905
|
);
|
|
904
906
|
};
|
|
905
907
|
|
|
908
|
+
/**
|
|
909
|
+
* Update track subscription configuration for one or more participants.
|
|
910
|
+
* You have to create a subscription for each participant for all the different kinds of tracks you want to receive.
|
|
911
|
+
* You can only subscribe for tracks after the participant started publishing the given kind of track.
|
|
912
|
+
*
|
|
913
|
+
* @param trackType the kind of subscription to update.
|
|
914
|
+
* @param changes the list of subscription changes to do.
|
|
915
|
+
* @param type the debounce type to use for the update.
|
|
916
|
+
*/
|
|
917
|
+
updateParticipantTracks = (
|
|
918
|
+
trackType: VideoTrackType,
|
|
919
|
+
changes: SubscriptionChanges,
|
|
920
|
+
) => {
|
|
921
|
+
return this.updateParticipants(
|
|
922
|
+
Object.entries(changes).reduce<StreamVideoParticipantPatches>(
|
|
923
|
+
(acc, [sessionId, change]) => {
|
|
924
|
+
if (change.dimension) {
|
|
925
|
+
change.dimension.height = Math.ceil(change.dimension.height);
|
|
926
|
+
change.dimension.width = Math.ceil(change.dimension.width);
|
|
927
|
+
}
|
|
928
|
+
const prop: keyof StreamVideoParticipant | undefined =
|
|
929
|
+
trackType === 'videoTrack'
|
|
930
|
+
? 'videoDimension'
|
|
931
|
+
: trackType === 'screenShareTrack'
|
|
932
|
+
? 'screenShareDimension'
|
|
933
|
+
: undefined;
|
|
934
|
+
if (prop) {
|
|
935
|
+
acc[sessionId] = {
|
|
936
|
+
[prop]: change.dimension,
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
return acc;
|
|
940
|
+
},
|
|
941
|
+
{},
|
|
942
|
+
),
|
|
943
|
+
);
|
|
944
|
+
};
|
|
945
|
+
|
|
906
946
|
/**
|
|
907
947
|
* Updates the call state with the data received from the server.
|
|
908
948
|
*
|