@stream-io/video-client 1.49.0 → 1.51.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 +22 -0
- package/dist/index.browser.es.js +1404 -682
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +1404 -682
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +1404 -682
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +43 -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/CameraManager.d.ts +1 -0
- package/dist/src/devices/DeviceManager.d.ts +23 -0
- package/dist/src/devices/DeviceManagerState.d.ts +0 -1
- package/dist/src/devices/VirtualDevice.d.ts +59 -0
- package/dist/src/devices/devicePersistence.d.ts +1 -1
- package/dist/src/devices/index.d.ts +1 -0
- 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/BasePeerConnection.d.ts +7 -2
- package/dist/src/rtc/Publisher.d.ts +38 -3
- package/dist/src/rtc/Subscriber.d.ts +1 -0
- package/dist/src/rtc/TransceiverCache.d.ts +5 -1
- package/dist/src/rtc/helpers/degradationPreference.d.ts +3 -0
- package/dist/src/rtc/types.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 +111 -33
- 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/CameraManager.ts +9 -2
- package/src/devices/DeviceManager.ts +239 -39
- package/src/devices/DeviceManagerState.ts +4 -2
- package/src/devices/VirtualDevice.ts +69 -0
- package/src/devices/__tests__/CameraManager.test.ts +19 -0
- package/src/devices/__tests__/DeviceManager.test.ts +404 -1
- package/src/devices/__tests__/mocks.ts +2 -0
- package/src/devices/devicePersistence.ts +2 -1
- package/src/devices/index.ts +1 -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/BasePeerConnection.ts +15 -3
- package/src/rtc/Publisher.ts +185 -40
- package/src/rtc/Subscriber.ts +42 -14
- package/src/rtc/TransceiverCache.ts +10 -3
- package/src/rtc/__tests__/Call.reconnect.test.ts +121 -2
- package/src/rtc/__tests__/Publisher.test.ts +747 -88
- package/src/rtc/__tests__/Subscriber.test.ts +148 -3
- package/src/rtc/__tests__/mocks/webrtc.mocks.ts +13 -2
- package/src/rtc/helpers/__tests__/degradationPreference.test.ts +55 -0
- package/src/rtc/helpers/degradationPreference.ts +40 -0
- package/src/rtc/types.ts +2 -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
package/src/Call.ts
CHANGED
|
@@ -145,7 +145,11 @@ import {
|
|
|
145
145
|
StatsReporter,
|
|
146
146
|
Tracer,
|
|
147
147
|
} from './stats';
|
|
148
|
+
import { AudioBindingsWatchdog } from './helpers/AudioBindingsWatchdog';
|
|
149
|
+
import { BlockedAudioTracker } from './helpers/BlockedAudioTracker';
|
|
150
|
+
import { TrackSubscriptionManager } from './helpers/TrackSubscriptionManager';
|
|
148
151
|
import { DynascaleManager } from './helpers/DynascaleManager';
|
|
152
|
+
import { ViewportTracker } from './helpers/ViewportTracker';
|
|
149
153
|
import { PermissionsContext } from './permissions';
|
|
150
154
|
import { CallTypes } from './CallType';
|
|
151
155
|
import { StreamClient } from './coordinator/connection/client';
|
|
@@ -230,7 +234,32 @@ export class Call {
|
|
|
230
234
|
/**
|
|
231
235
|
* The DynascaleManager instance.
|
|
232
236
|
*/
|
|
233
|
-
readonly dynascaleManager: DynascaleManager;
|
|
237
|
+
readonly dynascaleManager: DynascaleManager | undefined;
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Tracks viewport visibility for participant video elements.
|
|
241
|
+
* Available only in DOM environments.
|
|
242
|
+
*/
|
|
243
|
+
readonly viewportTracker: ViewportTracker | undefined;
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Owns the SFU-side video-subscription state (per-session and global overrides).
|
|
247
|
+
*/
|
|
248
|
+
readonly trackSubscriptionManager: TrackSubscriptionManager;
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Warns periodically when a remote participant is publishing audio, but no
|
|
252
|
+
* `<audio>` element has been bound for them.
|
|
253
|
+
*/
|
|
254
|
+
readonly audioBindingsWatchdog: AudioBindingsWatchdog | undefined;
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Tracks audio elements blocked by the browser's autoplay policy.
|
|
258
|
+
* Subscribe to `blockedAudioTracker.autoplayBlocked$` to react to the
|
|
259
|
+
* blocked state, and call {@link Call.resumeAudio} inside a user gesture
|
|
260
|
+
* to retry playback.
|
|
261
|
+
*/
|
|
262
|
+
readonly blockedAudioTracker: BlockedAudioTracker;
|
|
234
263
|
|
|
235
264
|
subscriber?: Subscriber;
|
|
236
265
|
publisher?: Publisher;
|
|
@@ -258,6 +287,7 @@ export class Call {
|
|
|
258
287
|
private statsReportingIntervalInMs: number = 2000;
|
|
259
288
|
private statsReporter?: StatsReporter;
|
|
260
289
|
private sfuStatsReporter?: SfuStatsReporter;
|
|
290
|
+
private lastStatsOptions?: StatsOptions;
|
|
261
291
|
private dropTimeout: ReturnType<typeof setTimeout> | undefined;
|
|
262
292
|
|
|
263
293
|
private readonly clientStore: StreamVideoWriteableStateStore;
|
|
@@ -362,11 +392,26 @@ export class Call {
|
|
|
362
392
|
this.microphone = new MicrophoneManager(this, preferences);
|
|
363
393
|
this.speaker = new SpeakerManager(this, preferences);
|
|
364
394
|
this.screenShare = new ScreenShareManager(this);
|
|
365
|
-
this.
|
|
395
|
+
this.trackSubscriptionManager = new TrackSubscriptionManager(
|
|
366
396
|
this.state,
|
|
367
|
-
this.speaker,
|
|
368
397
|
this.tracer,
|
|
369
398
|
);
|
|
399
|
+
this.blockedAudioTracker = new BlockedAudioTracker(this.tracer);
|
|
400
|
+
|
|
401
|
+
if (typeof document !== 'undefined') {
|
|
402
|
+
this.audioBindingsWatchdog = new AudioBindingsWatchdog(
|
|
403
|
+
this.state,
|
|
404
|
+
this.tracer,
|
|
405
|
+
);
|
|
406
|
+
this.viewportTracker = new ViewportTracker(this.state);
|
|
407
|
+
this.dynascaleManager = new DynascaleManager(
|
|
408
|
+
this.state,
|
|
409
|
+
this.speaker,
|
|
410
|
+
this.tracer,
|
|
411
|
+
this.trackSubscriptionManager,
|
|
412
|
+
this.blockedAudioTracker,
|
|
413
|
+
);
|
|
414
|
+
}
|
|
370
415
|
}
|
|
371
416
|
|
|
372
417
|
/**
|
|
@@ -692,17 +737,20 @@ export class Call {
|
|
|
692
737
|
this.sfuStatsReporter?.flush();
|
|
693
738
|
this.sfuStatsReporter?.stop();
|
|
694
739
|
this.sfuStatsReporter = undefined;
|
|
740
|
+
this.lastStatsOptions = undefined;
|
|
695
741
|
|
|
696
|
-
this.subscriber?.dispose();
|
|
742
|
+
await this.subscriber?.dispose();
|
|
697
743
|
this.subscriber = undefined;
|
|
698
744
|
|
|
699
|
-
this.publisher?.dispose();
|
|
745
|
+
await this.publisher?.dispose();
|
|
700
746
|
this.publisher = undefined;
|
|
701
747
|
|
|
702
748
|
await this.sfuClient?.leaveAndClose(leaveReason);
|
|
703
749
|
this.sfuClient = undefined;
|
|
704
|
-
this.
|
|
705
|
-
|
|
750
|
+
this.trackSubscriptionManager.setSfuClient(undefined);
|
|
751
|
+
this.trackSubscriptionManager.dispose();
|
|
752
|
+
this.audioBindingsWatchdog?.dispose();
|
|
753
|
+
await this.dynascaleManager?.dispose();
|
|
706
754
|
|
|
707
755
|
this.state.setCallingState(CallingState.LEFT);
|
|
708
756
|
this.state.setParticipants([]);
|
|
@@ -1079,17 +1127,19 @@ export class Call {
|
|
|
1079
1127
|
const performingFastReconnect =
|
|
1080
1128
|
this.reconnectStrategy === WebsocketReconnectStrategy.FAST;
|
|
1081
1129
|
|
|
1082
|
-
let statsOptions = this.
|
|
1130
|
+
let statsOptions = this.lastStatsOptions;
|
|
1083
1131
|
if (
|
|
1084
1132
|
!this.credentials ||
|
|
1085
1133
|
!statsOptions ||
|
|
1086
1134
|
performingRejoin ||
|
|
1087
|
-
performingMigration
|
|
1135
|
+
performingMigration ||
|
|
1136
|
+
data?.migrating_from
|
|
1088
1137
|
) {
|
|
1089
1138
|
try {
|
|
1090
1139
|
const joinResponse = await this.doJoinRequest(data);
|
|
1091
1140
|
this.credentials = joinResponse.credentials;
|
|
1092
1141
|
statsOptions = joinResponse.stats_options;
|
|
1142
|
+
this.lastStatsOptions = statsOptions;
|
|
1093
1143
|
} catch (error) {
|
|
1094
1144
|
// prevent triggering reconnect flow if the state is OFFLINE
|
|
1095
1145
|
const avoidRestoreState =
|
|
@@ -1126,7 +1176,7 @@ export class Call {
|
|
|
1126
1176
|
: previousSfuClient;
|
|
1127
1177
|
this.sfuClient = sfuClient;
|
|
1128
1178
|
this.unifiedSessionId ??= sfuClient.sessionId;
|
|
1129
|
-
this.
|
|
1179
|
+
this.trackSubscriptionManager.setSfuClient(sfuClient);
|
|
1130
1180
|
|
|
1131
1181
|
const clientDetails = await getClientDetails();
|
|
1132
1182
|
// we don't need to send JoinRequest if we are re-using an existing healthy SFU client
|
|
@@ -1216,7 +1266,7 @@ export class Call {
|
|
|
1216
1266
|
});
|
|
1217
1267
|
} else {
|
|
1218
1268
|
const connectionConfig = toRtcConfiguration(this.credentials.ice_servers);
|
|
1219
|
-
this.initPublisherAndSubscriber({
|
|
1269
|
+
await this.initPublisherAndSubscriber({
|
|
1220
1270
|
sfuClient,
|
|
1221
1271
|
connectionConfig,
|
|
1222
1272
|
clientDetails,
|
|
@@ -1293,7 +1343,7 @@ export class Call {
|
|
|
1293
1343
|
return {
|
|
1294
1344
|
strategy,
|
|
1295
1345
|
announcedTracks,
|
|
1296
|
-
subscriptions: this.
|
|
1346
|
+
subscriptions: this.trackSubscriptionManager.subscriptions,
|
|
1297
1347
|
reconnectAttempt: this.reconnectAttempts,
|
|
1298
1348
|
fromSfuId: migratingFromSfuId || '',
|
|
1299
1349
|
previousSessionId: performingRejoin ? previousSessionId || '' : '',
|
|
@@ -1390,7 +1440,7 @@ export class Call {
|
|
|
1390
1440
|
* Initializes the Publisher and Subscriber Peer Connections.
|
|
1391
1441
|
* @internal
|
|
1392
1442
|
*/
|
|
1393
|
-
private initPublisherAndSubscriber = (opts: {
|
|
1443
|
+
private initPublisherAndSubscriber = async (opts: {
|
|
1394
1444
|
sfuClient: StreamSfuClient;
|
|
1395
1445
|
connectionConfig: RTCConfiguration;
|
|
1396
1446
|
statsOptions: StatsOptions;
|
|
@@ -1410,7 +1460,7 @@ export class Call {
|
|
|
1410
1460
|
} = opts;
|
|
1411
1461
|
const { enable_rtc_stats: enableTracing } = statsOptions;
|
|
1412
1462
|
if (closePreviousInstances && this.subscriber) {
|
|
1413
|
-
this.subscriber.dispose();
|
|
1463
|
+
await this.subscriber.dispose();
|
|
1414
1464
|
}
|
|
1415
1465
|
const basePeerConnectionOptions: BasePeerConnectionOpts = {
|
|
1416
1466
|
sfuClient,
|
|
@@ -1441,7 +1491,7 @@ export class Call {
|
|
|
1441
1491
|
const isAnonymous = this.streamClient.user?.type === 'anonymous';
|
|
1442
1492
|
if (!isAnonymous) {
|
|
1443
1493
|
if (closePreviousInstances && this.publisher) {
|
|
1444
|
-
this.publisher.dispose();
|
|
1494
|
+
await this.publisher.dispose();
|
|
1445
1495
|
}
|
|
1446
1496
|
this.publisher = new Publisher(basePeerConnectionOptions, publishOptions);
|
|
1447
1497
|
}
|
|
@@ -1567,12 +1617,19 @@ export class Call {
|
|
|
1567
1617
|
reason: ReconnectReason,
|
|
1568
1618
|
): Promise<void> => {
|
|
1569
1619
|
if (
|
|
1620
|
+
this.state.callingState === CallingState.JOINING ||
|
|
1570
1621
|
this.state.callingState === CallingState.RECONNECTING ||
|
|
1571
1622
|
this.state.callingState === CallingState.MIGRATING ||
|
|
1572
1623
|
this.state.callingState === CallingState.RECONNECTING_FAILED
|
|
1573
1624
|
)
|
|
1574
1625
|
return;
|
|
1575
1626
|
|
|
1627
|
+
// Drop redundant reconnect calls. If a reconnect is already queued or
|
|
1628
|
+
// running for this Call, that entry will resolve whatever broke;
|
|
1629
|
+
// queueing more entries just replays the full REJOIN cycle (one extra
|
|
1630
|
+
// `POST /join` per entry) once the call is already healthy again.
|
|
1631
|
+
if (hasPending(this.reconnectConcurrencyTag)) return;
|
|
1632
|
+
|
|
1576
1633
|
return withoutConcurrency(this.reconnectConcurrencyTag, async () => {
|
|
1577
1634
|
const reconnectStartTime = Date.now();
|
|
1578
1635
|
this.reconnectStrategy = strategy;
|
|
@@ -1835,8 +1892,8 @@ export class Call {
|
|
|
1835
1892
|
// the `migrationTask`
|
|
1836
1893
|
this.state.setCallingState(CallingState.JOINED);
|
|
1837
1894
|
} finally {
|
|
1838
|
-
currentSubscriber?.dispose();
|
|
1839
|
-
currentPublisher?.dispose();
|
|
1895
|
+
await currentSubscriber?.dispose();
|
|
1896
|
+
await currentPublisher?.dispose();
|
|
1840
1897
|
|
|
1841
1898
|
// and close the previous SFU client, without specifying close code
|
|
1842
1899
|
currentSfuClient.close(StreamSfuClient.NORMAL_CLOSURE, 'Migrating away');
|
|
@@ -1976,7 +2033,7 @@ export class Call {
|
|
|
1976
2033
|
private restoreSubscribedTracks = () => {
|
|
1977
2034
|
const { remoteParticipants } = this.state;
|
|
1978
2035
|
if (remoteParticipants.length <= 0) return;
|
|
1979
|
-
this.
|
|
2036
|
+
this.trackSubscriptionManager.apply(undefined);
|
|
1980
2037
|
};
|
|
1981
2038
|
|
|
1982
2039
|
/**
|
|
@@ -2063,7 +2120,7 @@ export class Call {
|
|
|
2063
2120
|
*/
|
|
2064
2121
|
stopPublish = async (...trackTypes: TrackType[]) => {
|
|
2065
2122
|
if (!this.sfuClient || !this.publisher) return;
|
|
2066
|
-
this.publisher.stopTracks(...trackTypes);
|
|
2123
|
+
await this.publisher.stopTracks(...trackTypes);
|
|
2067
2124
|
await this.updateLocalStreamState(undefined, ...trackTypes);
|
|
2068
2125
|
};
|
|
2069
2126
|
|
|
@@ -2097,6 +2154,20 @@ export class Call {
|
|
|
2097
2154
|
}
|
|
2098
2155
|
};
|
|
2099
2156
|
|
|
2157
|
+
/**
|
|
2158
|
+
* Re-arms the encoder for a currently published track type. Useful for
|
|
2159
|
+
* working around WebKit's stalled sender bug after an iOS audio session
|
|
2160
|
+
* interruption (Siri, PSTN call).
|
|
2161
|
+
*
|
|
2162
|
+
* @internal
|
|
2163
|
+
*
|
|
2164
|
+
* @param trackType the track type to refresh.
|
|
2165
|
+
*/
|
|
2166
|
+
refreshPublishedTrack = async (trackType: TrackType) => {
|
|
2167
|
+
if (!this.publisher) return;
|
|
2168
|
+
await this.publisher.refreshTrack(trackType);
|
|
2169
|
+
};
|
|
2170
|
+
|
|
2100
2171
|
/**
|
|
2101
2172
|
* Updates the preferred publishing options
|
|
2102
2173
|
*
|
|
@@ -3010,7 +3081,7 @@ export class Call {
|
|
|
3010
3081
|
sessionId: string,
|
|
3011
3082
|
trackType: VideoTrackType,
|
|
3012
3083
|
) => {
|
|
3013
|
-
return this.
|
|
3084
|
+
return this.viewportTracker?.trackElementVisibility(
|
|
3014
3085
|
element,
|
|
3015
3086
|
sessionId,
|
|
3016
3087
|
trackType,
|
|
@@ -3023,7 +3094,7 @@ export class Call {
|
|
|
3023
3094
|
* @param element the viewport element.
|
|
3024
3095
|
*/
|
|
3025
3096
|
setViewport = <T extends HTMLElement>(element: T) => {
|
|
3026
|
-
return this.
|
|
3097
|
+
return this.viewportTracker?.setViewport(element);
|
|
3027
3098
|
};
|
|
3028
3099
|
|
|
3029
3100
|
/**
|
|
@@ -3046,7 +3117,7 @@ export class Call {
|
|
|
3046
3117
|
sessionId: string,
|
|
3047
3118
|
trackType: VideoTrackType,
|
|
3048
3119
|
) => {
|
|
3049
|
-
const unbind = this.dynascaleManager
|
|
3120
|
+
const unbind = this.dynascaleManager?.bindVideoElement(
|
|
3050
3121
|
videoElement,
|
|
3051
3122
|
sessionId,
|
|
3052
3123
|
trackType,
|
|
@@ -3075,26 +3146,33 @@ export class Call {
|
|
|
3075
3146
|
sessionId: string,
|
|
3076
3147
|
trackType: AudioTrackType = 'audioTrack',
|
|
3077
3148
|
) => {
|
|
3078
|
-
const unbind = this.dynascaleManager
|
|
3149
|
+
const unbind = this.dynascaleManager?.bindAudioElement(
|
|
3079
3150
|
audioElement,
|
|
3080
3151
|
sessionId,
|
|
3081
3152
|
trackType,
|
|
3082
3153
|
);
|
|
3083
3154
|
|
|
3084
3155
|
if (!unbind) return;
|
|
3085
|
-
this.
|
|
3086
|
-
|
|
3087
|
-
this.leaveCallHooks.delete(unbind);
|
|
3156
|
+
this.audioBindingsWatchdog?.register(audioElement, sessionId, trackType);
|
|
3157
|
+
const cleanup = () => {
|
|
3088
3158
|
unbind();
|
|
3159
|
+
this.audioBindingsWatchdog?.unregister(sessionId, trackType);
|
|
3160
|
+
};
|
|
3161
|
+
this.leaveCallHooks.add(cleanup);
|
|
3162
|
+
return () => {
|
|
3163
|
+
this.leaveCallHooks.delete(cleanup);
|
|
3164
|
+
cleanup();
|
|
3089
3165
|
};
|
|
3090
3166
|
};
|
|
3091
3167
|
|
|
3092
3168
|
/**
|
|
3093
3169
|
* Plays all audio elements blocked by the browser's autoplay policy.
|
|
3170
|
+
* Must be called from within a user gesture (e.g., click handler).
|
|
3171
|
+
*
|
|
3172
|
+
* Subscribe to `call.blockedAudioTracker.autoplayBlocked$` to know when a
|
|
3173
|
+
* gesture is required.
|
|
3094
3174
|
*/
|
|
3095
|
-
resumeAudio = () =>
|
|
3096
|
-
return this.dynascaleManager.resumeAudio();
|
|
3097
|
-
};
|
|
3175
|
+
resumeAudio = () => this.blockedAudioTracker.resumeAudio();
|
|
3098
3176
|
|
|
3099
3177
|
/**
|
|
3100
3178
|
* Binds a DOM <img> element to this call's thumbnail (if enabled in settings).
|
|
@@ -3148,7 +3226,7 @@ export class Call {
|
|
|
3148
3226
|
resolution: VideoDimension | undefined,
|
|
3149
3227
|
sessionIds?: string[],
|
|
3150
3228
|
) => {
|
|
3151
|
-
this.
|
|
3229
|
+
this.trackSubscriptionManager.setOverrides(
|
|
3152
3230
|
resolution
|
|
3153
3231
|
? {
|
|
3154
3232
|
enabled: true,
|
|
@@ -3157,7 +3235,7 @@ export class Call {
|
|
|
3157
3235
|
: undefined,
|
|
3158
3236
|
sessionIds,
|
|
3159
3237
|
);
|
|
3160
|
-
this.
|
|
3238
|
+
this.trackSubscriptionManager.apply();
|
|
3161
3239
|
};
|
|
3162
3240
|
|
|
3163
3241
|
/**
|
|
@@ -3165,10 +3243,10 @@ export class Call {
|
|
|
3165
3243
|
* and removes any preference for preferred resolution.
|
|
3166
3244
|
*/
|
|
3167
3245
|
setIncomingVideoEnabled = (enabled: boolean) => {
|
|
3168
|
-
this.
|
|
3246
|
+
this.trackSubscriptionManager.setOverrides(
|
|
3169
3247
|
enabled ? undefined : { enabled: false },
|
|
3170
3248
|
);
|
|
3171
|
-
this.
|
|
3249
|
+
this.trackSubscriptionManager.apply();
|
|
3172
3250
|
};
|
|
3173
3251
|
|
|
3174
3252
|
/**
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment happy-dom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import '../rtc/__tests__/mocks/webrtc.mocks';
|
|
6
|
+
|
|
7
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
8
|
+
import { Call } from '../Call';
|
|
9
|
+
import { StreamClient } from '../coordinator/connection/client';
|
|
10
|
+
import { generateUUIDv4 } from '../coordinator/connection/utils';
|
|
11
|
+
import { StreamVideoWriteableStateStore } from '../store';
|
|
12
|
+
|
|
13
|
+
describe('Call lifecycle wiring', () => {
|
|
14
|
+
let call: Call;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
call = new Call({
|
|
18
|
+
type: 'test',
|
|
19
|
+
id: generateUUIDv4(),
|
|
20
|
+
streamClient: new StreamClient('abc'),
|
|
21
|
+
clientStore: new StreamVideoWriteableStateStore(),
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Regression guard for the Call-owned helper teardown chain. Each of
|
|
26
|
+
// these helpers holds a resource (timer, listener, AudioContext) that
|
|
27
|
+
// leaks across calls if teardown is dropped during a refactor.
|
|
28
|
+
// Covers trackSubscriptionManager, audioBindingsWatchdog, and
|
|
29
|
+
// dynascaleManager. SFU-lifecycle disposables (publisher/subscriber/
|
|
30
|
+
// sfuStatsReporter) require a real join and are out of scope.
|
|
31
|
+
it('call.leave() tears down all Call-owned helpers exactly once', async () => {
|
|
32
|
+
const trackSubDispose = vi.spyOn(call.trackSubscriptionManager, 'dispose');
|
|
33
|
+
const audioBindingsDispose = vi.spyOn(
|
|
34
|
+
call.audioBindingsWatchdog!,
|
|
35
|
+
'dispose',
|
|
36
|
+
);
|
|
37
|
+
const dynascaleDispose = vi.spyOn(call.dynascaleManager!, 'dispose');
|
|
38
|
+
|
|
39
|
+
await call.leave();
|
|
40
|
+
|
|
41
|
+
expect(trackSubDispose).toHaveBeenCalledTimes(1);
|
|
42
|
+
expect(audioBindingsDispose).toHaveBeenCalledTimes(1);
|
|
43
|
+
expect(dynascaleDispose).toHaveBeenCalledTimes(1);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Order matters: the SFU subscription pump must finish tearing down
|
|
47
|
+
// before DynascaleManager closes its AudioContext, otherwise helpers
|
|
48
|
+
// can run on a closed context (logged as warnings or thrown by
|
|
49
|
+
// happy-dom). This is the contract the leave() teardown chain encodes.
|
|
50
|
+
it('call.leave() tears down helpers in the documented order', async () => {
|
|
51
|
+
const trackSubDispose = vi.spyOn(call.trackSubscriptionManager, 'dispose');
|
|
52
|
+
const audioBindingsDispose = vi.spyOn(
|
|
53
|
+
call.audioBindingsWatchdog!,
|
|
54
|
+
'dispose',
|
|
55
|
+
);
|
|
56
|
+
const dynascaleDispose = vi.spyOn(call.dynascaleManager!, 'dispose');
|
|
57
|
+
|
|
58
|
+
await call.leave();
|
|
59
|
+
|
|
60
|
+
const trackSubOrder = trackSubDispose.mock.invocationCallOrder[0];
|
|
61
|
+
const audioBindingsOrder = audioBindingsDispose.mock.invocationCallOrder[0];
|
|
62
|
+
const dynascaleOrder = dynascaleDispose.mock.invocationCallOrder[0];
|
|
63
|
+
|
|
64
|
+
expect(trackSubOrder).toBeLessThan(audioBindingsOrder);
|
|
65
|
+
expect(audioBindingsOrder).toBeLessThan(dynascaleOrder);
|
|
66
|
+
});
|
|
67
|
+
});
|