@stream-io/video-client 1.54.1-beta.0 → 1.55.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 +14 -0
- package/dist/index.browser.es.js +9672 -8865
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +9673 -8866
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +9674 -8867
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +4 -4
- package/dist/src/StreamSfuClient.d.ts +11 -3
- package/dist/src/coordinator/connection/connection.d.ts +1 -1
- package/dist/src/reporting/ClientEventReporter.d.ts +1 -0
- package/dist/src/rtc/BasePeerConnection.d.ts +2 -12
- package/dist/src/rtc/IceTrickleBuffer.d.ts +41 -3
- package/dist/src/rtc/Publisher.d.ts +1 -1
- package/dist/src/rtc/Subscriber.d.ts +2 -1
- package/dist/src/rtc/helpers/iceCandiates.d.ts +12 -0
- package/dist/src/rtc/types.d.ts +2 -0
- package/dist/src/stats/SfuStatsReporter.d.ts +32 -1
- package/dist/src/stats/rtc/StatsTracer.d.ts +38 -8
- package/dist/src/stats/rtc/Tracer.d.ts +9 -2
- package/dist/src/stats/rtc/types.d.ts +10 -4
- package/package.json +5 -3
- package/src/Call.ts +47 -44
- package/src/StreamSfuClient.ts +36 -21
- package/src/__tests__/StreamSfuClient.test.ts +159 -1
- package/src/__tests__/StreamVideoClient.api.test.ts +123 -97
- package/src/coordinator/connection/__tests__/connection.test.ts +22 -0
- package/src/coordinator/connection/connection.ts +8 -5
- package/src/gen/video/sfu/event/events.ts +0 -1
- package/src/gen/video/sfu/models/models.ts +0 -1
- package/src/gen/video/sfu/signal_rpc/signal.client.ts +0 -1
- package/src/gen/video/sfu/signal_rpc/signal.ts +0 -1
- package/src/helpers/MediaPlaybackWatchdog.ts +1 -0
- package/src/helpers/__tests__/browsers.test.ts +12 -12
- package/src/helpers/browsers.ts +5 -5
- package/src/helpers/client-details.ts +1 -1
- package/src/reporting/ClientEventReporter.ts +17 -12
- package/src/reporting/__tests__/ClientEventReporter.test.ts +52 -0
- package/src/rtc/BasePeerConnection.ts +15 -34
- package/src/rtc/IceTrickleBuffer.ts +105 -12
- package/src/rtc/Publisher.ts +19 -19
- package/src/rtc/Subscriber.ts +71 -37
- package/src/rtc/__tests__/Call.reconnect.test.ts +45 -45
- package/src/rtc/__tests__/IceTrickleBuffer.test.ts +127 -0
- package/src/rtc/__tests__/Publisher.test.ts +2 -31
- package/src/rtc/__tests__/Subscriber.test.ts +271 -20
- package/src/rtc/helpers/__tests__/iceCandiates.test.ts +88 -0
- package/src/rtc/helpers/degradationPreference.ts +1 -0
- package/src/rtc/helpers/iceCandiates.ts +35 -0
- package/src/rtc/helpers/sdp.ts +3 -2
- package/src/rtc/helpers/tracks.ts +2 -0
- package/src/rtc/types.ts +2 -0
- package/src/stats/SfuStatsReporter.ts +149 -49
- package/src/stats/__tests__/SfuStatsReporter.test.ts +235 -0
- package/src/stats/rtc/StatsTracer.ts +90 -32
- package/src/stats/rtc/Tracer.ts +23 -2
- package/src/stats/rtc/__tests__/StatsTracer.test.ts +213 -6
- package/src/stats/rtc/__tests__/Tracer.test.ts +34 -0
- package/src/stats/rtc/types.ts +11 -4
package/dist/src/Call.d.ts
CHANGED
|
@@ -129,7 +129,7 @@ export declare class Call {
|
|
|
129
129
|
private joinResponseTimeout?;
|
|
130
130
|
private rpcRequestTimeout?;
|
|
131
131
|
private joinCallData?;
|
|
132
|
-
private
|
|
132
|
+
private allowOwnTracksLoopback;
|
|
133
133
|
private hasJoinedOnce;
|
|
134
134
|
private deviceSettingsAppliedOnce;
|
|
135
135
|
private credentials?;
|
|
@@ -196,7 +196,7 @@ export declare class Call {
|
|
|
196
196
|
/**
|
|
197
197
|
* A flag indicating whether self-subscription is enabled for the call.
|
|
198
198
|
*/
|
|
199
|
-
get
|
|
199
|
+
get isOwnTracksLoopbackAllowed(): boolean;
|
|
200
200
|
/**
|
|
201
201
|
* The largest video publish dimension across the current publish options.
|
|
202
202
|
*
|
|
@@ -275,11 +275,11 @@ export declare class Call {
|
|
|
275
275
|
*
|
|
276
276
|
* @returns a promise which resolves once the call join-flow has finished.
|
|
277
277
|
*/
|
|
278
|
-
join: ({ maxJoinRetries, joinResponseTimeout, rpcRequestTimeout,
|
|
278
|
+
join: ({ maxJoinRetries, joinResponseTimeout, rpcRequestTimeout, allowOwnTracksLoopback, ...data }?: JoinCallData & {
|
|
279
279
|
maxJoinRetries?: number;
|
|
280
280
|
joinResponseTimeout?: number;
|
|
281
281
|
rpcRequestTimeout?: number;
|
|
282
|
-
|
|
282
|
+
allowOwnTracksLoopback?: boolean;
|
|
283
283
|
}) => Promise<void>;
|
|
284
284
|
/**
|
|
285
285
|
* Will make a single attempt to watch for call related WebSocket events
|
|
@@ -87,12 +87,20 @@ export declare class StreamSfuClient {
|
|
|
87
87
|
* trigger a reconnection attempt.
|
|
88
88
|
*/
|
|
89
89
|
isClosingClean: boolean;
|
|
90
|
+
/**
|
|
91
|
+
* One-shot latch guarding `onSignalClose`. The signal connection can be
|
|
92
|
+
* detected as dead by more than one source (the health watchdog and the
|
|
93
|
+
* WebSocket `close` event, which on a wedged socket can arrive seconds
|
|
94
|
+
* apart). This ensures revival is triggered at most once per client.
|
|
95
|
+
*/
|
|
96
|
+
private signalClosed;
|
|
90
97
|
private readonly rpc;
|
|
91
98
|
private keepAliveInterval?;
|
|
92
|
-
private
|
|
99
|
+
private connectionCheckInterval?;
|
|
93
100
|
private migrateAwayTimeout?;
|
|
94
101
|
private readonly pingIntervalInMs;
|
|
95
102
|
private readonly unhealthyTimeoutInMs;
|
|
103
|
+
private readonly connectionCheckIntervalInMs;
|
|
96
104
|
private lastMessageTimestamp?;
|
|
97
105
|
private readonly tracer?;
|
|
98
106
|
private readonly unsubscribeIceTrickle;
|
|
@@ -127,7 +135,7 @@ export declare class StreamSfuClient {
|
|
|
127
135
|
/**
|
|
128
136
|
* The error code used when the SFU connection is unhealthy.
|
|
129
137
|
* Usually, this means that no message has been received from the SFU for
|
|
130
|
-
* a certain amount of time (`
|
|
138
|
+
* a certain amount of time (`unhealthyTimeoutInMs`).
|
|
131
139
|
*/
|
|
132
140
|
static ERROR_CONNECTION_UNHEALTHY: number;
|
|
133
141
|
/**
|
|
@@ -154,7 +162,7 @@ export declare class StreamSfuClient {
|
|
|
154
162
|
private createWebSocket;
|
|
155
163
|
get isHealthy(): boolean;
|
|
156
164
|
get joinTask(): Promise<JoinResponse>;
|
|
157
|
-
private
|
|
165
|
+
private notifySignalClose;
|
|
158
166
|
close: (code?: number, reason?: string) => void;
|
|
159
167
|
private dispose;
|
|
160
168
|
getTrace: () => TraceSlice | undefined;
|
|
@@ -44,7 +44,7 @@ export declare class StableWSConnection {
|
|
|
44
44
|
pingInterval: number;
|
|
45
45
|
healthCheckTimeoutRef?: number;
|
|
46
46
|
connectionCheckTimeout: number;
|
|
47
|
-
connectionCheckTimeoutRef?:
|
|
47
|
+
connectionCheckTimeoutRef?: number;
|
|
48
48
|
/** Store the last event time for health checks */
|
|
49
49
|
lastEvent: Date | null;
|
|
50
50
|
constructor(client: StreamClient);
|
|
@@ -5,7 +5,7 @@ import { PeerType, TrackType } from '../gen/video/sfu/models/models';
|
|
|
5
5
|
import { StreamSfuClient } from '../StreamSfuClient';
|
|
6
6
|
import { AllSfuEvents, Dispatcher } from './Dispatcher';
|
|
7
7
|
import { StatsTracer, Tracer } from '../stats';
|
|
8
|
-
import { BasePeerConnectionOpts, OnRemoteTrackUnmute } from './types';
|
|
8
|
+
import { BasePeerConnectionOpts, OnReconnectionNeeded, OnRemoteTrackUnmute } from './types';
|
|
9
9
|
import type { ClientPublishOptions } from '../types';
|
|
10
10
|
/**
|
|
11
11
|
* A base class for the `Publisher` and `Subscriber` classes.
|
|
@@ -20,7 +20,7 @@ export declare abstract class BasePeerConnection {
|
|
|
20
20
|
protected readonly clientPublishOptions?: ClientPublishOptions;
|
|
21
21
|
protected tag: string;
|
|
22
22
|
protected sfuClient: StreamSfuClient;
|
|
23
|
-
|
|
23
|
+
protected onReconnectionNeeded?: OnReconnectionNeeded;
|
|
24
24
|
private onIceConnected?;
|
|
25
25
|
private onPeerConnectionStateChange?;
|
|
26
26
|
protected onRemoteTrackUnmute?: OnRemoteTrackUnmute;
|
|
@@ -95,21 +95,11 @@ export declare abstract class BasePeerConnection {
|
|
|
95
95
|
* it returns `false`, otherwise it returns `true`.
|
|
96
96
|
*/
|
|
97
97
|
isHealthy: () => boolean;
|
|
98
|
-
/**
|
|
99
|
-
* Returns true only when the peer connection is currently fully established
|
|
100
|
-
* (ICE `connected`/`completed` AND connection state `connected`).
|
|
101
|
-
* Transient states like `disconnected`, `checking`, or `new` return false.
|
|
102
|
-
*/
|
|
103
|
-
isStable: () => boolean;
|
|
104
98
|
/**
|
|
105
99
|
* Handles the ICECandidate event and
|
|
106
100
|
* Initiates an ICE Trickle process with the SFU.
|
|
107
101
|
*/
|
|
108
102
|
private onIceCandidate;
|
|
109
|
-
/**
|
|
110
|
-
* Converts the ICE candidate to a JSON string.
|
|
111
|
-
*/
|
|
112
|
-
private asJSON;
|
|
113
103
|
/**
|
|
114
104
|
* Handles the ConnectionStateChange event.
|
|
115
105
|
*/
|
|
@@ -1,12 +1,50 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Observable } from 'rxjs';
|
|
2
2
|
import { ICETrickle } from '../gen/video/sfu/event/events';
|
|
3
|
+
import { PeerType } from '../gen/video/sfu/models/models';
|
|
3
4
|
/**
|
|
4
5
|
* A buffer for ICE Candidates. Used for ICE Trickle:
|
|
5
6
|
* - https://bloggeek.me/webrtcglossary/trickle-ice/
|
|
7
|
+
*
|
|
8
|
+
* The buffer is generation-aware: each peer connection tells it which ICE
|
|
9
|
+
* generation is current via `updateActiveGeneration` (whenever it applies an
|
|
10
|
+
* offer/answer). Candidate streams then emit only candidates of the active
|
|
11
|
+
* generation, hold candidates of a not-yet-applied (future) generation until
|
|
12
|
+
* it becomes active, and drop candidates of a superseded generation so they
|
|
13
|
+
* are never replayed. Candidates with no detectable generation, or before any
|
|
14
|
+
* generation is set, are emitted as-is (fail open).
|
|
6
15
|
*/
|
|
7
16
|
export declare class IceTrickleBuffer {
|
|
8
|
-
readonly
|
|
9
|
-
readonly
|
|
17
|
+
readonly subscriber: CandidateGenerationBuffer;
|
|
18
|
+
readonly publisher: CandidateGenerationBuffer;
|
|
10
19
|
push: (iceTrickle: ICETrickle) => void;
|
|
20
|
+
/**
|
|
21
|
+
* Declares the ICE generation that is now current for the given peer type,
|
|
22
|
+
* derived from the `ice-ufrag` of the just-applied remote description.
|
|
23
|
+
* Candidates of superseded generations are evicted; candidates of the active
|
|
24
|
+
* generation flow to subscribers.
|
|
25
|
+
*/
|
|
26
|
+
updateActiveGeneration: (peerType: PeerType, sdp: string | undefined) => void;
|
|
11
27
|
dispose: () => void;
|
|
12
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Per-peer-connection generation-aware candidate store. Retains trickled
|
|
31
|
+
* candidates and replays the active generation to each new subscriber, then
|
|
32
|
+
* forwards matching live candidates.
|
|
33
|
+
*/
|
|
34
|
+
declare class CandidateGenerationBuffer {
|
|
35
|
+
private readonly store;
|
|
36
|
+
private readonly live;
|
|
37
|
+
private readonly seenUfrags;
|
|
38
|
+
private activeUfrag;
|
|
39
|
+
readonly candidates: Observable<RTCIceCandidateInit>;
|
|
40
|
+
push: (candidate: RTCIceCandidateInit) => void;
|
|
41
|
+
updateActiveGeneration: (ufrag: string | undefined) => void;
|
|
42
|
+
dispose: () => void;
|
|
43
|
+
/**
|
|
44
|
+
* A candidate belongs to the current generation when its ufrag matches the
|
|
45
|
+
* active one. Fail open when either the candidate's generation or the active
|
|
46
|
+
* generation is unknown, so untagged candidates are never withheld.
|
|
47
|
+
*/
|
|
48
|
+
private isCurrent;
|
|
49
|
+
}
|
|
50
|
+
export {};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { PublishOption, TrackInfo, TrackType } from '../gen/video/sfu/models/models';
|
|
1
2
|
import { BasePeerConnection } from './BasePeerConnection';
|
|
2
3
|
import type { BasePeerConnectionOpts, TrackPublishOptions } from './types';
|
|
3
|
-
import { PublishOption, TrackInfo, TrackType } from '../gen/video/sfu/models/models';
|
|
4
4
|
/**
|
|
5
5
|
* The `Publisher` is responsible for publishing/unpublishing media streams to/from the SFU
|
|
6
6
|
*
|
|
@@ -13,7 +13,8 @@ export declare class Subscriber extends BasePeerConnection {
|
|
|
13
13
|
* The map will never contain local streams so we can safely use it to
|
|
14
14
|
* check if the stream is remote and dispose it when needed.
|
|
15
15
|
*/
|
|
16
|
-
private trackedStreams
|
|
16
|
+
private trackedStreams?;
|
|
17
|
+
private negotiationFailures;
|
|
17
18
|
/**
|
|
18
19
|
* Constructs a new `Subscriber` instance.
|
|
19
20
|
*/
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts the ICE candidate to a JSON string.
|
|
3
|
+
*/
|
|
4
|
+
export declare const toJSON: (candidate: RTCIceCandidate) => string;
|
|
5
|
+
/**
|
|
6
|
+
* Extracts the ICE ufrag from an SDP, or `undefined` when absent.
|
|
7
|
+
*/
|
|
8
|
+
export declare const parseIceUfrag: (sdp: string | undefined) => string | undefined;
|
|
9
|
+
/**
|
|
10
|
+
* Extracts the ICE ufrag (generation) a trickled candidate was gathered under.
|
|
11
|
+
*/
|
|
12
|
+
export declare const getCandidateUfrag: (ice: RTCIceCandidateInit) => string | undefined;
|
package/dist/src/rtc/types.d.ts
CHANGED
|
@@ -21,6 +21,8 @@ export declare const ReconnectReason: {
|
|
|
21
21
|
readonly CONNECTION_FAILED: "connection_failed";
|
|
22
22
|
/** `restartIce()` rejected. */
|
|
23
23
|
readonly RESTART_ICE_FAILED: "restart_ice_failed";
|
|
24
|
+
/** Subscriber renegotiation kept failing, escalate to REJOIN. */
|
|
25
|
+
readonly SUBSCRIBER_NEGOTIATION_FAILED: "subscriber_negotiation_failed";
|
|
24
26
|
/** SFU `goAway` event, migrate to a new SFU. */
|
|
25
27
|
readonly GO_AWAY: "go_away";
|
|
26
28
|
/** Network came back online after going offline. */
|
|
@@ -36,14 +36,45 @@ export declare class SfuStatsReporter {
|
|
|
36
36
|
private readonly sdkVersion;
|
|
37
37
|
private readonly webRTCVersion;
|
|
38
38
|
private readonly inputDevices;
|
|
39
|
+
private readonly statsConcurrencyTag;
|
|
40
|
+
private isStopped;
|
|
39
41
|
constructor(sfuClient: StreamSfuClient, { options, clientDetails, subscriber, publisher, microphone, camera, state, tracer, unifiedSessionId, }: SfuStatsReporterOptions);
|
|
40
42
|
private observeDevice;
|
|
41
43
|
sendConnectionTime: (connectionTimeSeconds: number) => void;
|
|
42
44
|
sendReconnectionTime: (strategy: WebsocketReconnectStrategy, timeSeconds: number) => void;
|
|
43
45
|
private sendTelemetryData;
|
|
46
|
+
/**
|
|
47
|
+
* Samples both peer connections. Each `StatsTracer.takeSample()` is serialized
|
|
48
|
+
* internally, so this is safe even if it overlaps another sample (e.g. the
|
|
49
|
+
* connection-state-change handler). Kept separate from `send()` so an
|
|
50
|
+
* explicit flush can capture the sample from live peer connections before
|
|
51
|
+
* they are disposed, without waiting for an in-flight send.
|
|
52
|
+
*/
|
|
53
|
+
private sample;
|
|
54
|
+
private send;
|
|
55
|
+
/**
|
|
56
|
+
* Samples and sends one report. Used by the scheduler and the telemetry path.
|
|
57
|
+
* Bails if the reporter has been stopped so it never samples disposed peer
|
|
58
|
+
* connections.
|
|
59
|
+
*/
|
|
44
60
|
private run;
|
|
45
61
|
private scheduleNextReport;
|
|
46
62
|
start: () => void;
|
|
47
63
|
stop: () => void;
|
|
48
|
-
|
|
64
|
+
/**
|
|
65
|
+
* Explicit/final flush (leave, migration, re-init). Time-boxes the sampling
|
|
66
|
+
* step and swallows its failures, so a slow or failing `getStats()` on a
|
|
67
|
+
* degraded or closing peer connection can never block or reject call teardown
|
|
68
|
+
* or reconnect setup. On a successful sample it fires the send best-effort;
|
|
69
|
+
* the returned promise resolves once the sample is taken (or the time-box
|
|
70
|
+
* elapses / sampling fails), never when the sending completes. No-op once the
|
|
71
|
+
* reporter has been stopped.
|
|
72
|
+
*/
|
|
73
|
+
flush: () => Promise<void>;
|
|
74
|
+
/**
|
|
75
|
+
* Flush entry for the periodic scheduler. Skips when the reporter is stopped
|
|
76
|
+
* or a send is already in flight so ticks can't pile up under slow sends (the
|
|
77
|
+
* next sample's delta spans the skipped interval).
|
|
78
|
+
*/
|
|
79
|
+
private scheduledFlush;
|
|
49
80
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { PeerType, TrackType } from '../../gen/video/sfu/models/models';
|
|
2
|
-
import type { ComputedStats } from './types';
|
|
2
|
+
import type { ComputedStats, PendingDelta } from './types';
|
|
3
3
|
/**
|
|
4
4
|
* StatsTracer is a class that collects and processes WebRTC stats.
|
|
5
5
|
* It is used to track the performance of the WebRTC connection
|
|
@@ -13,23 +13,53 @@ export declare class StatsTracer {
|
|
|
13
13
|
private readonly peerType;
|
|
14
14
|
private readonly trackIdToTrackType;
|
|
15
15
|
private readonly driftThresholdMs;
|
|
16
|
+
private readonly maxPendingDeltas;
|
|
17
|
+
private readonly sampleTag;
|
|
16
18
|
private costOverrides?;
|
|
17
|
-
private
|
|
19
|
+
private previousSample;
|
|
18
20
|
private frameTimeHistory;
|
|
19
21
|
private fpsHistory;
|
|
22
|
+
/**
|
|
23
|
+
* The un-acked, delta-compressed samples awaiting delivery confirmation.
|
|
24
|
+
* Each entry's delta is computed against the immediately preceding sample,
|
|
25
|
+
* so the list forms a chain the server applies in order. The delivery
|
|
26
|
+
* baseline advances only when the reporter calls `commitDeltas` after a
|
|
27
|
+
* successful send; until then the chain is re-sent in full.
|
|
28
|
+
*/
|
|
29
|
+
private pendingDeltas;
|
|
20
30
|
/**
|
|
21
31
|
* Creates a new StatsTracer instance.
|
|
22
32
|
*/
|
|
23
|
-
constructor(pc: RTCPeerConnection, peerType: PeerType, trackIdToTrackType: Map<string, TrackType>, statsTimestampDriftThresholdMs?: number);
|
|
33
|
+
constructor(pc: RTCPeerConnection, peerType: PeerType, trackIdToTrackType: Map<string, TrackType>, statsTimestampDriftThresholdMs?: number, maxPendingDeltas?: number);
|
|
34
|
+
/**
|
|
35
|
+
* Samples the RTCPeerConnection: returns the current stats report and the
|
|
36
|
+
* derived performance stats, and appends the delta-compressed sample to the
|
|
37
|
+
* un-acked delivery chain (retrieved via `getPendingDeltas`).
|
|
38
|
+
*
|
|
39
|
+
* @internal
|
|
40
|
+
*/
|
|
41
|
+
takeSample: () => Promise<ComputedStats>;
|
|
42
|
+
/**
|
|
43
|
+
* Returns a stable copy of the un-acked delta chain to transmit, oldest first.
|
|
44
|
+
*
|
|
45
|
+
* @internal
|
|
46
|
+
*/
|
|
47
|
+
getPendingDeltas: () => PendingDelta[];
|
|
48
|
+
/**
|
|
49
|
+
* Advances the delivery baseline by dropping exactly the deltas that were
|
|
50
|
+
* confirmed delivered. Matching is by object identity, so a stale commit
|
|
51
|
+
* that arrives after a re-anchor (which replaced the chain) is a safe no-op.
|
|
52
|
+
*
|
|
53
|
+
* @internal
|
|
54
|
+
*/
|
|
55
|
+
commitDeltas: (sent: PendingDelta[]) => void;
|
|
24
56
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* It will also return the delta between the current stats and the previous stats.
|
|
28
|
-
* This is used to track the performance of the connection.
|
|
57
|
+
* Drops the un-acked chain without sending it. Used when delta reporting is
|
|
58
|
+
* disabled so the chain can't grow unbounded.
|
|
29
59
|
*
|
|
30
60
|
* @internal
|
|
31
61
|
*/
|
|
32
|
-
|
|
62
|
+
clearPendingDeltas: () => void;
|
|
33
63
|
/**
|
|
34
64
|
* Collects encode stats from the RTCPeerConnection.
|
|
35
65
|
*/
|
|
@@ -2,13 +2,20 @@ import type { RTCStatsDataType, Trace, TraceKey, TraceSlice } from './types';
|
|
|
2
2
|
export declare class Tracer {
|
|
3
3
|
private buffer;
|
|
4
4
|
private enabled;
|
|
5
|
-
|
|
5
|
+
readonly id: string | null;
|
|
6
|
+
private readonly maxBuffer;
|
|
6
7
|
private keys?;
|
|
7
|
-
constructor(id: string | null);
|
|
8
|
+
constructor(id: string | null, maxBuffer?: number);
|
|
8
9
|
setEnabled: (enabled: boolean) => void;
|
|
9
10
|
trace: Trace;
|
|
10
11
|
traceOnce: (key: TraceKey, tag: string, data: RTCStatsDataType) => void;
|
|
11
12
|
resetTrace: (key: TraceKey) => void;
|
|
12
13
|
take: () => TraceSlice;
|
|
13
14
|
dispose: () => void;
|
|
15
|
+
/**
|
|
16
|
+
* Bounds the buffer to 2500 records by dropping the oldest ones,
|
|
17
|
+
* leaving a single `traceBufferOverflow` breadcrumb at the front so the
|
|
18
|
+
* consumer knows records were dropped.
|
|
19
|
+
*/
|
|
20
|
+
private capBuffer;
|
|
14
21
|
}
|
|
@@ -17,12 +17,18 @@ export type ComputedStats = {
|
|
|
17
17
|
* Current stats from the RTCPeerConnection.
|
|
18
18
|
*/
|
|
19
19
|
stats: RTCStatsReport;
|
|
20
|
-
/**
|
|
21
|
-
* Delta between the current stats and the previous stats.
|
|
22
|
-
*/
|
|
23
|
-
delta: Record<any, any>;
|
|
24
20
|
/**
|
|
25
21
|
* The current iteration of the stats.
|
|
26
22
|
*/
|
|
27
23
|
performanceStats: PerformanceStats[];
|
|
28
24
|
};
|
|
25
|
+
/**
|
|
26
|
+
* A single, not-yet-delivered delta-compressed stats sample.
|
|
27
|
+
* The `delta` is computed against the immediately preceding sample, so a
|
|
28
|
+
* sequence of `PendingDelta`s forms a chain that the server applies in order
|
|
29
|
+
* onto its running accumulator. `ts` is the wall-clock time the sample was taken.
|
|
30
|
+
*/
|
|
31
|
+
export type PendingDelta = {
|
|
32
|
+
delta: Record<any, any>;
|
|
33
|
+
ts: number;
|
|
34
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stream-io/video-client",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.55.0",
|
|
4
4
|
"main": "dist/index.cjs.js",
|
|
5
5
|
"module": "dist/index.es.js",
|
|
6
6
|
"browser": "dist/index.browser.es.js",
|
|
@@ -46,9 +46,11 @@
|
|
|
46
46
|
"@openapitools/openapi-generator-cli": "^2.34.0",
|
|
47
47
|
"@rollup/plugin-replace": "^6.0.3",
|
|
48
48
|
"@rollup/plugin-typescript": "^12.3.0",
|
|
49
|
-
"@stream-io/audio-filters-web": "^0.
|
|
49
|
+
"@stream-io/audio-filters-web": "^0.9.0",
|
|
50
50
|
"@stream-io/node-sdk": "^0.7.59",
|
|
51
|
+
"@stream-io/typescript-config": "^0.0.0",
|
|
51
52
|
"@total-typescript/shoehorn": "^0.1.2",
|
|
53
|
+
"@types/node": "^25.9.3",
|
|
52
54
|
"@types/sdp-transform": "^2.15.0",
|
|
53
55
|
"@types/ua-parser-js": "^0.7.39",
|
|
54
56
|
"@vitest/coverage-v8": "^4.1.7",
|
|
@@ -57,7 +59,7 @@
|
|
|
57
59
|
"prettier": "^3.8.3",
|
|
58
60
|
"rimraf": "^6.1.3",
|
|
59
61
|
"rollup": "^4.60.4",
|
|
60
|
-
"typescript": "^
|
|
62
|
+
"typescript": "^6.0.3",
|
|
61
63
|
"vitest": "^4.1.7",
|
|
62
64
|
"vitest-mock-extended": "^4.0.0"
|
|
63
65
|
}
|
package/src/Call.ts
CHANGED
|
@@ -325,7 +325,7 @@ export class Call {
|
|
|
325
325
|
private joinResponseTimeout?: number;
|
|
326
326
|
private rpcRequestTimeout?: number;
|
|
327
327
|
private joinCallData?: JoinCallData;
|
|
328
|
-
private
|
|
328
|
+
private allowOwnTracksLoopback = false;
|
|
329
329
|
private hasJoinedOnce = false;
|
|
330
330
|
private deviceSettingsAppliedOnce = false;
|
|
331
331
|
private credentials?: Credentials;
|
|
@@ -741,7 +741,9 @@ export class Call {
|
|
|
741
741
|
|
|
742
742
|
const leaveReason = message ?? reason ?? 'user is leaving the call';
|
|
743
743
|
this.tracer.trace('call.leaveReason', leaveReason);
|
|
744
|
-
|
|
744
|
+
// await the final sample so it's captured from the still-live peer
|
|
745
|
+
// connections (disposed below); the send itself stays best-effort.
|
|
746
|
+
await this.sfuStatsReporter?.flush();
|
|
745
747
|
this.sfuStatsReporter?.stop();
|
|
746
748
|
this.sfuStatsReporter = undefined;
|
|
747
749
|
this.lastStatsOptions = undefined;
|
|
@@ -788,6 +790,7 @@ export class Call {
|
|
|
788
790
|
this.leaveCallHooks.forEach((hook) => hook());
|
|
789
791
|
this.initialized = false;
|
|
790
792
|
this.hasJoinedOnce = false;
|
|
793
|
+
this.allowOwnTracksLoopback = false;
|
|
791
794
|
this.unifiedSessionId = undefined;
|
|
792
795
|
this.ringingSubject.next(false);
|
|
793
796
|
this.cancelAutoDrop();
|
|
@@ -834,8 +837,8 @@ export class Call {
|
|
|
834
837
|
/**
|
|
835
838
|
* A flag indicating whether self-subscription is enabled for the call.
|
|
836
839
|
*/
|
|
837
|
-
get
|
|
838
|
-
return this.
|
|
840
|
+
get isOwnTracksLoopbackAllowed() {
|
|
841
|
+
return this.allowOwnTracksLoopback;
|
|
839
842
|
}
|
|
840
843
|
|
|
841
844
|
/**
|
|
@@ -1074,13 +1077,13 @@ export class Call {
|
|
|
1074
1077
|
maxJoinRetries = 3,
|
|
1075
1078
|
joinResponseTimeout,
|
|
1076
1079
|
rpcRequestTimeout,
|
|
1077
|
-
|
|
1080
|
+
allowOwnTracksLoopback = false,
|
|
1078
1081
|
...data
|
|
1079
1082
|
}: JoinCallData & {
|
|
1080
1083
|
maxJoinRetries?: number;
|
|
1081
1084
|
joinResponseTimeout?: number;
|
|
1082
1085
|
rpcRequestTimeout?: number;
|
|
1083
|
-
|
|
1086
|
+
allowOwnTracksLoopback?: boolean;
|
|
1084
1087
|
} = {}): Promise<void> => {
|
|
1085
1088
|
const callingState = this.state.callingState;
|
|
1086
1089
|
|
|
@@ -1088,14 +1091,14 @@ export class Call {
|
|
|
1088
1091
|
throw new Error(`Illegal State: call.join() shall be called only once`);
|
|
1089
1092
|
}
|
|
1090
1093
|
|
|
1094
|
+
// we need this to be set before the callingx.joinCall() is
|
|
1095
|
+
// called to avoid registering the test call in the CallKit/Telecom
|
|
1096
|
+
this.allowOwnTracksLoopback = allowOwnTracksLoopback;
|
|
1097
|
+
|
|
1091
1098
|
if (data?.ring) {
|
|
1092
1099
|
this.ringingSubject.next(true);
|
|
1093
1100
|
}
|
|
1094
1101
|
|
|
1095
|
-
// we need this to be set before the callingx.joinCall() is
|
|
1096
|
-
// called to avoid registering the test call in the CallKit/Telecom
|
|
1097
|
-
this.selfSubEnabled = selfSubEnabled;
|
|
1098
|
-
|
|
1099
1102
|
const callingX = globalThis.streamRNVideoSDK?.callingX;
|
|
1100
1103
|
if (callingX) {
|
|
1101
1104
|
// for Android/iOS, we need to start the call in the callingx library as soon as possible
|
|
@@ -1330,23 +1333,9 @@ export class Call {
|
|
|
1330
1333
|
// when performing fast reconnect, or when we reuse the same SFU client,
|
|
1331
1334
|
// (ws remained healthy), we just need to restore the ICE connection
|
|
1332
1335
|
if (performingFastReconnect) {
|
|
1333
|
-
//
|
|
1334
|
-
//
|
|
1335
|
-
|
|
1336
|
-
// the signal WebSocket drop was the only problem — the new WS alone
|
|
1337
|
-
// is enough, and restarting ICE would add unnecessary SDP/ICE churn.
|
|
1338
|
-
const publisherIsStable = this.publisher?.isStable() ?? true;
|
|
1339
|
-
const includePublisher =
|
|
1340
|
-
!!this.publisher?.isPublishing() && !publisherIsStable;
|
|
1341
|
-
if (!includePublisher && this.publisher?.isPublishing()) {
|
|
1342
|
-
this.logger.info(
|
|
1343
|
-
'[Reconnect] FAST: skipping publisher ICE restart, publisher PC is stable',
|
|
1344
|
-
);
|
|
1345
|
-
}
|
|
1346
|
-
await this.restoreICE(sfuClient, {
|
|
1347
|
-
includeSubscriber: false,
|
|
1348
|
-
includePublisher,
|
|
1349
|
-
});
|
|
1336
|
+
// the SFU automatically issues an ICE restart on the subscriber
|
|
1337
|
+
// we don't have to do it ourselves
|
|
1338
|
+
await this.restoreICE(sfuClient, { includeSubscriber: false });
|
|
1350
1339
|
} else {
|
|
1351
1340
|
const connectionConfig = toRtcConfiguration(this.credentials.ice_servers);
|
|
1352
1341
|
await this.initPublisherAndSubscriber({
|
|
@@ -1545,6 +1534,11 @@ export class Call {
|
|
|
1545
1534
|
enable_rtc_stats: enableTracing,
|
|
1546
1535
|
reporting_interval_ms: reportingIntervalMs,
|
|
1547
1536
|
} = statsOptions;
|
|
1537
|
+
// Flush the previous reporter's final sample while its peer connections are
|
|
1538
|
+
// still alive, before we dispose them below. Awaits only the sampling step.
|
|
1539
|
+
await this.sfuStatsReporter?.flush();
|
|
1540
|
+
this.sfuStatsReporter?.stop();
|
|
1541
|
+
this.sfuStatsReporter = undefined;
|
|
1548
1542
|
if (closePreviousInstances && this.subscriber) {
|
|
1549
1543
|
await this.subscriber.dispose();
|
|
1550
1544
|
}
|
|
@@ -1596,7 +1590,7 @@ export class Call {
|
|
|
1596
1590
|
basePeerConnectionOptions,
|
|
1597
1591
|
publishOptions,
|
|
1598
1592
|
{
|
|
1599
|
-
selfSubEnabled: this.
|
|
1593
|
+
selfSubEnabled: this.allowOwnTracksLoopback,
|
|
1600
1594
|
},
|
|
1601
1595
|
);
|
|
1602
1596
|
}
|
|
@@ -1613,8 +1607,6 @@ export class Call {
|
|
|
1613
1607
|
}
|
|
1614
1608
|
|
|
1615
1609
|
this.tracer.setEnabled(enableTracing);
|
|
1616
|
-
this.sfuStatsReporter?.flush();
|
|
1617
|
-
this.sfuStatsReporter?.stop();
|
|
1618
1610
|
if (statsOptions?.reporting_interval_ms > 0) {
|
|
1619
1611
|
this.sfuStatsReporter = new SfuStatsReporter(sfuClient, {
|
|
1620
1612
|
clientDetails,
|
|
@@ -1682,6 +1674,10 @@ export class Call {
|
|
|
1682
1674
|
reason: string,
|
|
1683
1675
|
) => {
|
|
1684
1676
|
this.logger.debug('[Reconnect] SFU signal connection closed');
|
|
1677
|
+
// Ignore a close from a superseded client (e.g. an old socket's delayed
|
|
1678
|
+
// `onclose` arriving after a reconnection already swapped in a new client).
|
|
1679
|
+
// Only the currently active client's death may drive a reconnection.
|
|
1680
|
+
if (sfuClient !== this.sfuClient) return;
|
|
1685
1681
|
const { callingState } = this.state;
|
|
1686
1682
|
if (
|
|
1687
1683
|
// SFU WS closed before we finished current join,
|
|
@@ -1722,11 +1718,12 @@ export class Call {
|
|
|
1722
1718
|
strategy: WebsocketReconnectStrategy,
|
|
1723
1719
|
reason: ReconnectReason,
|
|
1724
1720
|
): Promise<void> => {
|
|
1721
|
+
const { callingState } = this.state;
|
|
1725
1722
|
if (
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1723
|
+
callingState === CallingState.JOINING ||
|
|
1724
|
+
callingState === CallingState.RECONNECTING ||
|
|
1725
|
+
callingState === CallingState.MIGRATING ||
|
|
1726
|
+
callingState === CallingState.RECONNECTING_FAILED
|
|
1730
1727
|
)
|
|
1731
1728
|
return;
|
|
1732
1729
|
|
|
@@ -2138,10 +2135,17 @@ export class Call {
|
|
|
2138
2135
|
} else {
|
|
2139
2136
|
if (!this.networkAvailableTask) return;
|
|
2140
2137
|
this.logger.debug('[Reconnect] Going online');
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2138
|
+
// Only discard the socket when no reconnect is already in flight. A
|
|
2139
|
+
// running reconnect owns the SFU-client lifecycle (`doJoin` closes
|
|
2140
|
+
// the previous client once the new session is up). Closing here would
|
|
2141
|
+
// tear down the socket it's mid-join on, leaving the in-flight rejoin
|
|
2142
|
+
// to spin on SIGNAL_LOST.
|
|
2143
|
+
if (!hasPending(this.reconnectConcurrencyTag)) {
|
|
2144
|
+
this.sfuClient?.close(
|
|
2145
|
+
StreamSfuClient.DISPOSE_OLD_SOCKET,
|
|
2146
|
+
'Closing WS to reconnect after going online',
|
|
2147
|
+
);
|
|
2148
|
+
}
|
|
2145
2149
|
// we went online, release the previous waiters and reset the state
|
|
2146
2150
|
this.networkAvailableTask?.resolve();
|
|
2147
2151
|
this.networkAvailableTask = undefined;
|
|
@@ -2495,9 +2499,8 @@ export class Call {
|
|
|
2495
2499
|
*/
|
|
2496
2500
|
muteSelf = (type: TrackMuteType) => {
|
|
2497
2501
|
const myUserId = this.currentUserId;
|
|
2498
|
-
if (myUserId)
|
|
2499
|
-
|
|
2500
|
-
}
|
|
2502
|
+
if (!myUserId) return undefined;
|
|
2503
|
+
return this.muteUser(myUserId, type);
|
|
2501
2504
|
};
|
|
2502
2505
|
|
|
2503
2506
|
/**
|
|
@@ -2515,9 +2518,9 @@ export class Call {
|
|
|
2515
2518
|
}
|
|
2516
2519
|
}
|
|
2517
2520
|
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
+
return userIdsToMute.length > 0
|
|
2522
|
+
? this.muteUser(userIdsToMute, type)
|
|
2523
|
+
: undefined;
|
|
2521
2524
|
};
|
|
2522
2525
|
|
|
2523
2526
|
/**
|