@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
|
@@ -1,36 +1,30 @@
|
|
|
1
|
+
import type { CallState } from '../store';
|
|
2
|
+
import { VideoTrackType } from '../types';
|
|
1
3
|
export type EntryHandler = (entry: IntersectionObserverEntry) => void;
|
|
2
4
|
export type Unobserve = () => void;
|
|
3
5
|
export type Observe = (element: HTMLElement, entryHandler: EntryHandler) => Unobserve;
|
|
4
6
|
export declare class ViewportTracker {
|
|
5
|
-
|
|
6
|
-
* @private
|
|
7
|
-
*/
|
|
7
|
+
private callState;
|
|
8
8
|
private elementHandlerMap;
|
|
9
|
-
/**
|
|
10
|
-
* @private
|
|
11
|
-
*/
|
|
12
9
|
private observer;
|
|
13
|
-
/**
|
|
14
|
-
* @private
|
|
15
|
-
*/
|
|
16
10
|
private queueSet;
|
|
11
|
+
constructor(callState: CallState);
|
|
17
12
|
/**
|
|
18
13
|
* Method to set scrollable viewport as root for the IntersectionObserver, returns
|
|
19
14
|
* cleanup function to be invoked upon disposing of the DOM element to prevent memory leaks
|
|
20
|
-
*
|
|
21
|
-
* @param viewportElement
|
|
22
|
-
* @param options
|
|
23
|
-
* @returns Unobserve
|
|
24
15
|
*/
|
|
25
16
|
setViewport: (viewportElement: HTMLElement, options?: Pick<IntersectionObserverInit, "threshold" | "rootMargin">) => () => void;
|
|
26
17
|
/**
|
|
27
18
|
* Method to set element to observe and handler to be triggered whenever IntersectionObserver
|
|
28
19
|
* detects a possible change in element's visibility within specified viewport, returns
|
|
29
20
|
* cleanup function to be invoked upon disposing of the DOM element to prevent memory leaks
|
|
30
|
-
*
|
|
31
|
-
* @param element
|
|
32
|
-
* @param handler
|
|
33
|
-
* @returns Unobserve
|
|
34
21
|
*/
|
|
35
22
|
observe: Observe;
|
|
23
|
+
/**
|
|
24
|
+
* Tracks the given element for visibility changes and mirrors the result
|
|
25
|
+
* into `participant.viewportVisibilityState[trackType]` in `CallState`.
|
|
26
|
+
* Returns a function that unobserves the element and resets the visibility
|
|
27
|
+
* state back to `UNKNOWN`.
|
|
28
|
+
*/
|
|
29
|
+
trackElementVisibility: <T extends HTMLElement>(element: T, sessionId: string, trackType: VideoTrackType) => () => void;
|
|
36
30
|
}
|
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
* Checks whether the current browser is Safari.
|
|
3
3
|
*/
|
|
4
4
|
export declare const isSafari: () => boolean;
|
|
5
|
+
/**
|
|
6
|
+
* Checks whether the current runtime is a WebKit-engine browser.
|
|
7
|
+
*
|
|
8
|
+
* Returns true for desktop Safari, iOS Safari, bare iOS WKWebView hosts
|
|
9
|
+
* (in-app browsers, React Native WebView, Tauri-on-macOS, etc.), and for
|
|
10
|
+
* Chromium / Gecko-branded iOS browsers (`CriOS`, `EdgiOS`, `OPiOS`,
|
|
11
|
+
* `FxiOS`) since Apple forces every iOS browser onto WKWebView and they
|
|
12
|
+
* share the underlying WebKit quirks.
|
|
13
|
+
*
|
|
14
|
+
* Returns false for desktop Chromium-based browsers (which reuse the
|
|
15
|
+
* `AppleWebKit/` token in their UA) and Android.
|
|
16
|
+
*/
|
|
17
|
+
export declare const isWebKit: () => boolean;
|
|
5
18
|
/**
|
|
6
19
|
* Checks whether the current browser is Firefox.
|
|
7
20
|
*/
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
type TagKey = string | symbol | object;
|
|
1
2
|
/**
|
|
2
3
|
* Runs async functions serially. Useful for wrapping async actions that
|
|
3
4
|
* should never run simultaneously: if marked with the same tag, functions
|
|
@@ -8,7 +9,7 @@
|
|
|
8
9
|
* @param cb Async function to run.
|
|
9
10
|
* @returns Promise that resolves when async functions returns.
|
|
10
11
|
*/
|
|
11
|
-
export declare const withoutConcurrency: <T>(tag:
|
|
12
|
+
export declare const withoutConcurrency: <T>(tag: TagKey, cb: () => Promise<T>) => Promise<T>;
|
|
12
13
|
/**
|
|
13
14
|
* Runs async functions serially, and cancels all other actions with the same tag
|
|
14
15
|
* when a new action is scheduled. Useful for wrapping async actions that override
|
|
@@ -25,6 +26,7 @@ export declare const withoutConcurrency: <T>(tag: string | symbol, cb: () => Pro
|
|
|
25
26
|
* start and was canceled, will resolve with 'canceled'. If the function started to run,
|
|
26
27
|
* it's up to the function to decide how to react to cancelation.
|
|
27
28
|
*/
|
|
28
|
-
export declare const withCancellation: <T>(tag:
|
|
29
|
-
export declare function hasPending(tag:
|
|
30
|
-
export declare function settled(tag:
|
|
29
|
+
export declare const withCancellation: <T>(tag: TagKey, cb: (signal: AbortSignal) => Promise<T | "canceled">) => Promise<T | "canceled">;
|
|
30
|
+
export declare function hasPending(tag: TagKey): boolean;
|
|
31
|
+
export declare function settled(tag: TagKey): Promise<void>;
|
|
32
|
+
export {};
|
|
@@ -51,6 +51,23 @@ export declare class Publisher extends BasePeerConnection {
|
|
|
51
51
|
* @param trackType the track type to check. If omitted, checks if any track is being published.
|
|
52
52
|
*/
|
|
53
53
|
isPublishing: (trackType?: TrackType) => boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Re-arms the encoder for the given track type by detaching and
|
|
56
|
+
* reattaching the currently published track on each matching sender.
|
|
57
|
+
*
|
|
58
|
+
* Workaround for a WebKit / iOS Safari quirk: after a system audio
|
|
59
|
+
* session interruption (Siri, PSTN call), the `RTCRtpSender` encoder
|
|
60
|
+
* can stop producing RTP packets even though the underlying
|
|
61
|
+
* `MediaStreamTrack` is `live` and `track.muted === false`.
|
|
62
|
+
* `replaceTrack(null)` followed by `replaceTrack(track)` resets the
|
|
63
|
+
* sender's encoder pipeline without renegotiation, restoring packet
|
|
64
|
+
* flow with the same SSRC.
|
|
65
|
+
*
|
|
66
|
+
* No-op when nothing is published for the given track type.
|
|
67
|
+
*
|
|
68
|
+
* @param trackType the track type to refresh.
|
|
69
|
+
*/
|
|
70
|
+
refreshTrack: (trackType: TrackType) => Promise<void>;
|
|
54
71
|
/**
|
|
55
72
|
* Stops the cloned track that is being published to the SFU.
|
|
56
73
|
*/
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { PerformanceStats } from '../../gen/video/sfu/models/models';
|
|
2
|
-
export type RTCStatsDataType = RTCConfiguration | RTCIceCandidate | RTCSignalingState | RTCIceConnectionState | RTCIceGatheringState | RTCPeerConnectionState | [number | null | string] | string | boolean | RTCOfferOptions | [string | RTCDataChannelInit | undefined] | (RTCOfferOptions | undefined) | RTCSessionDescriptionInit | (RTCIceCandidateInit | RTCIceCandidate) | object | null | undefined;
|
|
2
|
+
export type RTCStatsDataType = RTCConfiguration | RTCIceCandidate | RTCSignalingState | RTCIceConnectionState | RTCIceGatheringState | RTCPeerConnectionState | [number | null | string] | string | boolean | RTCOfferOptions | [string | RTCDataChannelInit | undefined] | (RTCOfferOptions | undefined) | RTCSessionDescriptionInit | (RTCIceCandidateInit | RTCIceCandidate) | object | number | null | undefined;
|
|
3
3
|
export type TraceKey = 'device-enumeration' | (string & {});
|
|
4
4
|
export type Trace = (tag: string, data: RTCStatsDataType) => void;
|
|
5
5
|
export type TraceRecord = [
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { BehaviorSubject, Observable, Subject } from 'rxjs';
|
|
2
2
|
type FunctionPatch<T> = (currentValue: T) => T;
|
|
3
|
+
type AsyncFunctionPatch<T> = (currentValue: T) => Promise<T>;
|
|
3
4
|
/**
|
|
4
5
|
* A value or a function which takes the current value and returns a new value.
|
|
5
6
|
*/
|
|
@@ -21,6 +22,14 @@ export declare const getCurrentValue: <T>(observable$: Observable<T>) => T;
|
|
|
21
22
|
* @return the updated value.
|
|
22
23
|
*/
|
|
23
24
|
export declare const setCurrentValue: <T>(subject: Subject<T>, update: Patch<T>) => T;
|
|
25
|
+
/**
|
|
26
|
+
* Updates the value of the provided Subject asynchronously.
|
|
27
|
+
* Locks the subject to prevent concurrent updates.
|
|
28
|
+
*
|
|
29
|
+
* @param subject the subject to update.
|
|
30
|
+
* @param update the update to apply to the subject.
|
|
31
|
+
*/
|
|
32
|
+
export declare const setCurrentValueAsync: <T>(subject: Subject<T>, update: AsyncFunctionPatch<T>) => Promise<T>;
|
|
24
33
|
/**
|
|
25
34
|
* Updates the value of the provided Subject and returns the previous value
|
|
26
35
|
* and a function to roll back the update.
|
package/dist/src/types.d.ts
CHANGED
|
@@ -58,6 +58,24 @@ export interface StreamVideoParticipant extends Participant {
|
|
|
58
58
|
* This is useful to avoid any unwanted video and audio artifacts.
|
|
59
59
|
*/
|
|
60
60
|
pausedTracks?: TrackType[];
|
|
61
|
+
/**
|
|
62
|
+
* The list of tracks that are currently not producing media.
|
|
63
|
+
*
|
|
64
|
+
* For remote participants this is currently surfaced for `TrackType.AUDIO`
|
|
65
|
+
* only and reflects the receiver-side `RTCRtpReceiver` track `mute`/`unmute`
|
|
66
|
+
* state, so it covers system mute on the sender (OS audio session
|
|
67
|
+
* interruption, etc.), the sender pausing its track, sustained RTP stalls,
|
|
68
|
+
* and SFU drops. Remote video and screen-share interruption is not tracked.
|
|
69
|
+
*
|
|
70
|
+
* For the local participant it reflects the local track `mute`/`unmute`
|
|
71
|
+
* events surfaced by the browser (e.g. bluetooth disconnect, OS-level
|
|
72
|
+
* mic/camera kill switch, iOS audio session interruption).
|
|
73
|
+
*
|
|
74
|
+
* Orthogonal to `publishedTracks`: a track can be in `publishedTracks`
|
|
75
|
+
* AND in `interruptedTracks` (the participant intends to publish, but
|
|
76
|
+
* no media is flowing right now).
|
|
77
|
+
*/
|
|
78
|
+
interruptedTracks?: TrackType[];
|
|
61
79
|
/**
|
|
62
80
|
* True if the participant is the local participant.
|
|
63
81
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stream-io/video-client",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.50.0",
|
|
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.25.0",
|
|
47
47
|
"@rollup/plugin-replace": "^6.0.2",
|
|
48
48
|
"@rollup/plugin-typescript": "^12.1.4",
|
|
49
|
-
"@stream-io/audio-filters-web": "^0.
|
|
49
|
+
"@stream-io/audio-filters-web": "^0.8.0",
|
|
50
50
|
"@stream-io/node-sdk": "^0.7.28",
|
|
51
51
|
"@total-typescript/shoehorn": "^0.1.2",
|
|
52
52
|
"@types/sdp-transform": "^2.15.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;
|
|
@@ -362,11 +391,26 @@ export class Call {
|
|
|
362
391
|
this.microphone = new MicrophoneManager(this, preferences);
|
|
363
392
|
this.speaker = new SpeakerManager(this, preferences);
|
|
364
393
|
this.screenShare = new ScreenShareManager(this);
|
|
365
|
-
this.
|
|
394
|
+
this.trackSubscriptionManager = new TrackSubscriptionManager(
|
|
366
395
|
this.state,
|
|
367
|
-
this.speaker,
|
|
368
396
|
this.tracer,
|
|
369
397
|
);
|
|
398
|
+
this.blockedAudioTracker = new BlockedAudioTracker(this.tracer);
|
|
399
|
+
|
|
400
|
+
if (typeof document !== 'undefined') {
|
|
401
|
+
this.audioBindingsWatchdog = new AudioBindingsWatchdog(
|
|
402
|
+
this.state,
|
|
403
|
+
this.tracer,
|
|
404
|
+
);
|
|
405
|
+
this.viewportTracker = new ViewportTracker(this.state);
|
|
406
|
+
this.dynascaleManager = new DynascaleManager(
|
|
407
|
+
this.state,
|
|
408
|
+
this.speaker,
|
|
409
|
+
this.tracer,
|
|
410
|
+
this.trackSubscriptionManager,
|
|
411
|
+
this.blockedAudioTracker,
|
|
412
|
+
);
|
|
413
|
+
}
|
|
370
414
|
}
|
|
371
415
|
|
|
372
416
|
/**
|
|
@@ -701,8 +745,10 @@ export class Call {
|
|
|
701
745
|
|
|
702
746
|
await this.sfuClient?.leaveAndClose(leaveReason);
|
|
703
747
|
this.sfuClient = undefined;
|
|
704
|
-
this.
|
|
705
|
-
|
|
748
|
+
this.trackSubscriptionManager.setSfuClient(undefined);
|
|
749
|
+
this.trackSubscriptionManager.dispose();
|
|
750
|
+
this.audioBindingsWatchdog?.dispose();
|
|
751
|
+
await this.dynascaleManager?.dispose();
|
|
706
752
|
|
|
707
753
|
this.state.setCallingState(CallingState.LEFT);
|
|
708
754
|
this.state.setParticipants([]);
|
|
@@ -1126,7 +1172,7 @@ export class Call {
|
|
|
1126
1172
|
: previousSfuClient;
|
|
1127
1173
|
this.sfuClient = sfuClient;
|
|
1128
1174
|
this.unifiedSessionId ??= sfuClient.sessionId;
|
|
1129
|
-
this.
|
|
1175
|
+
this.trackSubscriptionManager.setSfuClient(sfuClient);
|
|
1130
1176
|
|
|
1131
1177
|
const clientDetails = await getClientDetails();
|
|
1132
1178
|
// we don't need to send JoinRequest if we are re-using an existing healthy SFU client
|
|
@@ -1293,7 +1339,7 @@ export class Call {
|
|
|
1293
1339
|
return {
|
|
1294
1340
|
strategy,
|
|
1295
1341
|
announcedTracks,
|
|
1296
|
-
subscriptions: this.
|
|
1342
|
+
subscriptions: this.trackSubscriptionManager.subscriptions,
|
|
1297
1343
|
reconnectAttempt: this.reconnectAttempts,
|
|
1298
1344
|
fromSfuId: migratingFromSfuId || '',
|
|
1299
1345
|
previousSessionId: performingRejoin ? previousSessionId || '' : '',
|
|
@@ -1976,7 +2022,7 @@ export class Call {
|
|
|
1976
2022
|
private restoreSubscribedTracks = () => {
|
|
1977
2023
|
const { remoteParticipants } = this.state;
|
|
1978
2024
|
if (remoteParticipants.length <= 0) return;
|
|
1979
|
-
this.
|
|
2025
|
+
this.trackSubscriptionManager.apply(undefined);
|
|
1980
2026
|
};
|
|
1981
2027
|
|
|
1982
2028
|
/**
|
|
@@ -2097,6 +2143,20 @@ export class Call {
|
|
|
2097
2143
|
}
|
|
2098
2144
|
};
|
|
2099
2145
|
|
|
2146
|
+
/**
|
|
2147
|
+
* Re-arms the encoder for a currently published track type. Useful for
|
|
2148
|
+
* working around WebKit's stalled sender bug after an iOS audio session
|
|
2149
|
+
* interruption (Siri, PSTN call).
|
|
2150
|
+
*
|
|
2151
|
+
* @internal
|
|
2152
|
+
*
|
|
2153
|
+
* @param trackType the track type to refresh.
|
|
2154
|
+
*/
|
|
2155
|
+
refreshPublishedTrack = async (trackType: TrackType) => {
|
|
2156
|
+
if (!this.publisher) return;
|
|
2157
|
+
await this.publisher.refreshTrack(trackType);
|
|
2158
|
+
};
|
|
2159
|
+
|
|
2100
2160
|
/**
|
|
2101
2161
|
* Updates the preferred publishing options
|
|
2102
2162
|
*
|
|
@@ -3010,7 +3070,7 @@ export class Call {
|
|
|
3010
3070
|
sessionId: string,
|
|
3011
3071
|
trackType: VideoTrackType,
|
|
3012
3072
|
) => {
|
|
3013
|
-
return this.
|
|
3073
|
+
return this.viewportTracker?.trackElementVisibility(
|
|
3014
3074
|
element,
|
|
3015
3075
|
sessionId,
|
|
3016
3076
|
trackType,
|
|
@@ -3023,7 +3083,7 @@ export class Call {
|
|
|
3023
3083
|
* @param element the viewport element.
|
|
3024
3084
|
*/
|
|
3025
3085
|
setViewport = <T extends HTMLElement>(element: T) => {
|
|
3026
|
-
return this.
|
|
3086
|
+
return this.viewportTracker?.setViewport(element);
|
|
3027
3087
|
};
|
|
3028
3088
|
|
|
3029
3089
|
/**
|
|
@@ -3046,7 +3106,7 @@ export class Call {
|
|
|
3046
3106
|
sessionId: string,
|
|
3047
3107
|
trackType: VideoTrackType,
|
|
3048
3108
|
) => {
|
|
3049
|
-
const unbind = this.dynascaleManager
|
|
3109
|
+
const unbind = this.dynascaleManager?.bindVideoElement(
|
|
3050
3110
|
videoElement,
|
|
3051
3111
|
sessionId,
|
|
3052
3112
|
trackType,
|
|
@@ -3075,26 +3135,33 @@ export class Call {
|
|
|
3075
3135
|
sessionId: string,
|
|
3076
3136
|
trackType: AudioTrackType = 'audioTrack',
|
|
3077
3137
|
) => {
|
|
3078
|
-
const unbind = this.dynascaleManager
|
|
3138
|
+
const unbind = this.dynascaleManager?.bindAudioElement(
|
|
3079
3139
|
audioElement,
|
|
3080
3140
|
sessionId,
|
|
3081
3141
|
trackType,
|
|
3082
3142
|
);
|
|
3083
3143
|
|
|
3084
3144
|
if (!unbind) return;
|
|
3085
|
-
this.
|
|
3086
|
-
|
|
3087
|
-
this.leaveCallHooks.delete(unbind);
|
|
3145
|
+
this.audioBindingsWatchdog?.register(audioElement, sessionId, trackType);
|
|
3146
|
+
const cleanup = () => {
|
|
3088
3147
|
unbind();
|
|
3148
|
+
this.audioBindingsWatchdog?.unregister(sessionId, trackType);
|
|
3149
|
+
};
|
|
3150
|
+
this.leaveCallHooks.add(cleanup);
|
|
3151
|
+
return () => {
|
|
3152
|
+
this.leaveCallHooks.delete(cleanup);
|
|
3153
|
+
cleanup();
|
|
3089
3154
|
};
|
|
3090
3155
|
};
|
|
3091
3156
|
|
|
3092
3157
|
/**
|
|
3093
3158
|
* Plays all audio elements blocked by the browser's autoplay policy.
|
|
3159
|
+
* Must be called from within a user gesture (e.g., click handler).
|
|
3160
|
+
*
|
|
3161
|
+
* Subscribe to `call.blockedAudioTracker.autoplayBlocked$` to know when a
|
|
3162
|
+
* gesture is required.
|
|
3094
3163
|
*/
|
|
3095
|
-
resumeAudio = () =>
|
|
3096
|
-
return this.dynascaleManager.resumeAudio();
|
|
3097
|
-
};
|
|
3164
|
+
resumeAudio = () => this.blockedAudioTracker.resumeAudio();
|
|
3098
3165
|
|
|
3099
3166
|
/**
|
|
3100
3167
|
* Binds a DOM <img> element to this call's thumbnail (if enabled in settings).
|
|
@@ -3148,7 +3215,7 @@ export class Call {
|
|
|
3148
3215
|
resolution: VideoDimension | undefined,
|
|
3149
3216
|
sessionIds?: string[],
|
|
3150
3217
|
) => {
|
|
3151
|
-
this.
|
|
3218
|
+
this.trackSubscriptionManager.setOverrides(
|
|
3152
3219
|
resolution
|
|
3153
3220
|
? {
|
|
3154
3221
|
enabled: true,
|
|
@@ -3157,7 +3224,7 @@ export class Call {
|
|
|
3157
3224
|
: undefined,
|
|
3158
3225
|
sessionIds,
|
|
3159
3226
|
);
|
|
3160
|
-
this.
|
|
3227
|
+
this.trackSubscriptionManager.apply();
|
|
3161
3228
|
};
|
|
3162
3229
|
|
|
3163
3230
|
/**
|
|
@@ -3165,10 +3232,10 @@ export class Call {
|
|
|
3165
3232
|
* and removes any preference for preferred resolution.
|
|
3166
3233
|
*/
|
|
3167
3234
|
setIncomingVideoEnabled = (enabled: boolean) => {
|
|
3168
|
-
this.
|
|
3235
|
+
this.trackSubscriptionManager.setOverrides(
|
|
3169
3236
|
enabled ? undefined : { enabled: false },
|
|
3170
3237
|
);
|
|
3171
|
-
this.
|
|
3238
|
+
this.trackSubscriptionManager.apply();
|
|
3172
3239
|
};
|
|
3173
3240
|
|
|
3174
3241
|
/**
|
|
@@ -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
|
+
});
|