@stream-io/video-client 1.54.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.
Files changed (64) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/index.browser.es.js +9641 -8767
  3. package/dist/index.browser.es.js.map +1 -1
  4. package/dist/index.cjs.js +9638 -8764
  5. package/dist/index.cjs.js.map +1 -1
  6. package/dist/index.es.js +9639 -8765
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/src/Call.d.ts +13 -1
  9. package/dist/src/StreamSfuClient.d.ts +11 -3
  10. package/dist/src/coordinator/connection/connection.d.ts +1 -1
  11. package/dist/src/gen/google/protobuf/struct.d.ts +3 -1
  12. package/dist/src/gen/google/protobuf/timestamp.d.ts +3 -1
  13. package/dist/src/gen/video/sfu/event/events.d.ts +22 -1
  14. package/dist/src/gen/video/sfu/models/models.d.ts +4 -0
  15. package/dist/src/gen/video/sfu/signal_rpc/signal.client.d.ts +23 -2
  16. package/dist/src/reporting/ClientEventReporter.d.ts +1 -0
  17. package/dist/src/rtc/BasePeerConnection.d.ts +2 -12
  18. package/dist/src/rtc/IceTrickleBuffer.d.ts +41 -3
  19. package/dist/src/rtc/Publisher.d.ts +5 -2
  20. package/dist/src/rtc/Subscriber.d.ts +8 -0
  21. package/dist/src/rtc/helpers/iceCandiates.d.ts +12 -0
  22. package/dist/src/rtc/types.d.ts +2 -0
  23. package/dist/src/stats/SfuStatsReporter.d.ts +32 -1
  24. package/dist/src/stats/rtc/StatsTracer.d.ts +38 -8
  25. package/dist/src/stats/rtc/Tracer.d.ts +9 -2
  26. package/dist/src/stats/rtc/types.d.ts +10 -4
  27. package/package.json +5 -3
  28. package/src/Call.ts +83 -35
  29. package/src/StreamSfuClient.ts +36 -21
  30. package/src/__tests__/StreamSfuClient.test.ts +159 -1
  31. package/src/__tests__/StreamVideoClient.api.test.ts +123 -97
  32. package/src/coordinator/connection/__tests__/connection.test.ts +22 -0
  33. package/src/coordinator/connection/connection.ts +8 -5
  34. package/src/gen/google/protobuf/struct.ts +7 -12
  35. package/src/gen/google/protobuf/timestamp.ts +6 -7
  36. package/src/gen/video/sfu/event/events.ts +22 -25
  37. package/src/gen/video/sfu/models/models.ts +10 -1
  38. package/src/gen/video/sfu/signal_rpc/signal.client.ts +24 -29
  39. package/src/helpers/MediaPlaybackWatchdog.ts +1 -0
  40. package/src/helpers/__tests__/browsers.test.ts +12 -12
  41. package/src/helpers/browsers.ts +5 -5
  42. package/src/reporting/ClientEventReporter.ts +17 -12
  43. package/src/reporting/__tests__/ClientEventReporter.test.ts +52 -0
  44. package/src/rtc/BasePeerConnection.ts +15 -34
  45. package/src/rtc/IceTrickleBuffer.ts +105 -12
  46. package/src/rtc/Publisher.ts +23 -19
  47. package/src/rtc/Subscriber.ts +97 -36
  48. package/src/rtc/__tests__/Call.reconnect.test.ts +45 -45
  49. package/src/rtc/__tests__/IceTrickleBuffer.test.ts +127 -0
  50. package/src/rtc/__tests__/Publisher.test.ts +2 -31
  51. package/src/rtc/__tests__/Subscriber.test.ts +271 -20
  52. package/src/rtc/helpers/__tests__/iceCandiates.test.ts +88 -0
  53. package/src/rtc/helpers/degradationPreference.ts +1 -0
  54. package/src/rtc/helpers/iceCandiates.ts +35 -0
  55. package/src/rtc/helpers/sdp.ts +3 -2
  56. package/src/rtc/helpers/tracks.ts +2 -0
  57. package/src/rtc/types.ts +2 -0
  58. package/src/stats/SfuStatsReporter.ts +149 -49
  59. package/src/stats/__tests__/SfuStatsReporter.test.ts +235 -0
  60. package/src/stats/rtc/StatsTracer.ts +90 -32
  61. package/src/stats/rtc/Tracer.ts +23 -2
  62. package/src/stats/rtc/__tests__/StatsTracer.test.ts +213 -6
  63. package/src/stats/rtc/__tests__/Tracer.test.ts +34 -0
  64. package/src/stats/rtc/types.ts +11 -4
@@ -129,6 +129,7 @@ export declare class Call {
129
129
  private joinResponseTimeout?;
130
130
  private rpcRequestTimeout?;
131
131
  private joinCallData?;
132
+ private allowOwnTracksLoopback;
132
133
  private hasJoinedOnce;
133
134
  private deviceSettingsAppliedOnce;
134
135
  private credentials?;
@@ -192,6 +193,16 @@ export declare class Call {
192
193
  * Retrieves the current user ID.
193
194
  */
194
195
  get currentUserId(): string | undefined;
196
+ /**
197
+ * A flag indicating whether self-subscription is enabled for the call.
198
+ */
199
+ get isOwnTracksLoopbackAllowed(): boolean;
200
+ /**
201
+ * The largest video publish dimension across the current publish options.
202
+ *
203
+ * @internal
204
+ */
205
+ getMaxVideoPublishDimension: () => VideoDimension | undefined;
195
206
  /**
196
207
  * A flag indicating whether the call was created by the current user.
197
208
  */
@@ -264,10 +275,11 @@ export declare class Call {
264
275
  *
265
276
  * @returns a promise which resolves once the call join-flow has finished.
266
277
  */
267
- join: ({ maxJoinRetries, joinResponseTimeout, rpcRequestTimeout, ...data }?: JoinCallData & {
278
+ join: ({ maxJoinRetries, joinResponseTimeout, rpcRequestTimeout, allowOwnTracksLoopback, ...data }?: JoinCallData & {
268
279
  maxJoinRetries?: number;
269
280
  joinResponseTimeout?: number;
270
281
  rpcRequestTimeout?: number;
282
+ allowOwnTracksLoopback?: boolean;
271
283
  }) => Promise<void>;
272
284
  /**
273
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 connectionCheckTimeout?;
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 (`connectionCheckTimeout`).
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 handleWebSocketClose;
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?: NodeJS.Timeout;
47
+ connectionCheckTimeoutRef?: number;
48
48
  /** Store the last event time for health checks */
49
49
  lastEvent: Date | null;
50
50
  constructor(client: StreamClient);
@@ -1,4 +1,6 @@
1
- import type { JsonReadOptions, JsonValue, JsonWriteOptions } from '@protobuf-ts/runtime';
1
+ import type { JsonValue } from '@protobuf-ts/runtime';
2
+ import type { JsonReadOptions } from '@protobuf-ts/runtime';
3
+ import type { JsonWriteOptions } from '@protobuf-ts/runtime';
2
4
  import { MessageType } from '@protobuf-ts/runtime';
3
5
  /**
4
6
  * `Struct` represents a structured data value, consisting of fields
@@ -1,4 +1,6 @@
1
- import type { JsonReadOptions, JsonValue, JsonWriteOptions } from '@protobuf-ts/runtime';
1
+ import type { JsonValue } from '@protobuf-ts/runtime';
2
+ import type { JsonReadOptions } from '@protobuf-ts/runtime';
3
+ import type { JsonWriteOptions } from '@protobuf-ts/runtime';
2
4
  import { MessageType } from '@protobuf-ts/runtime';
3
5
  /**
4
6
  * A Timestamp represents a point in time independent of any time zone or local
@@ -1,6 +1,27 @@
1
1
  import { MessageType } from '@protobuf-ts/runtime';
2
- import { CallEndedReason, CallGrants, CallState, ClientCapability, ClientDetails, Codec, ConnectionQuality, DegradationPreference, Error as Error$, GoAwayReason, ICETrickle as ICETrickle$, Participant, ParticipantCount, ParticipantSource, PeerType, Pin, PublishOption, SubscribeOption, TrackInfo, TrackType, TrackUnpublishReason, WebsocketReconnectStrategy } from '../models/models';
2
+ import { CallEndedReason } from '../models/models';
3
+ import { GoAwayReason } from '../models/models';
4
+ import { CallGrants } from '../models/models';
5
+ import { DegradationPreference } from '../models/models';
6
+ import { Codec } from '../models/models';
7
+ import { ConnectionQuality } from '../models/models';
8
+ import { CallState } from '../models/models';
3
9
  import { TrackSubscriptionDetails } from '../signal_rpc/signal';
10
+ import { TrackInfo } from '../models/models';
11
+ import { ParticipantSource } from '../models/models';
12
+ import { ClientCapability } from '../models/models';
13
+ import { SubscribeOption } from '../models/models';
14
+ import { ClientDetails } from '../models/models';
15
+ import { TrackUnpublishReason } from '../models/models';
16
+ import { Participant } from '../models/models';
17
+ import { TrackType } from '../models/models';
18
+ import { ParticipantCount } from '../models/models';
19
+ import { PeerType } from '../models/models';
20
+ import { WebsocketReconnectStrategy } from '../models/models';
21
+ import { Error as Error$ } from '../models/models';
22
+ import { Pin } from '../models/models';
23
+ import { PublishOption } from '../models/models';
24
+ import { ICETrickle as ICETrickle$ } from '../models/models';
4
25
  /**
5
26
  * SFUEvent is a message that is sent from the SFU to the client.
6
27
  *
@@ -411,6 +411,10 @@ export interface TrackInfo {
411
411
  * @generated from protobuf field: int32 publish_option_id = 12;
412
412
  */
413
413
  publishOptionId: number;
414
+ /**
415
+ * @generated from protobuf field: bool self_sub_audio_video = 13;
416
+ */
417
+ selfSubAudioVideo: boolean;
414
418
  }
415
419
  /**
416
420
  * @generated from protobuf message stream.video.sfu.models.Error
@@ -1,6 +1,27 @@
1
- import type { RpcOptions, RpcTransport, ServiceInfo, UnaryCall } from '@protobuf-ts/runtime-rpc';
2
- import type { ICERestartRequest, ICERestartResponse, ICETrickleResponse, SendAnswerRequest, SendAnswerResponse, SendMetricsRequest, SendMetricsResponse, SendStatsRequest, SendStatsResponse, SetPublisherRequest, SetPublisherResponse, StartNoiseCancellationRequest, StartNoiseCancellationResponse, StopNoiseCancellationRequest, StopNoiseCancellationResponse, UpdateMuteStatesRequest, UpdateMuteStatesResponse, UpdateSubscriptionsRequest, UpdateSubscriptionsResponse } from './signal';
1
+ import type { RpcTransport } from '@protobuf-ts/runtime-rpc';
2
+ import type { ServiceInfo } from '@protobuf-ts/runtime-rpc';
3
+ import type { StopNoiseCancellationResponse } from './signal';
4
+ import type { StopNoiseCancellationRequest } from './signal';
5
+ import type { StartNoiseCancellationResponse } from './signal';
6
+ import type { StartNoiseCancellationRequest } from './signal';
7
+ import type { SendMetricsResponse } from './signal';
8
+ import type { SendMetricsRequest } from './signal';
9
+ import type { SendStatsResponse } from './signal';
10
+ import type { SendStatsRequest } from './signal';
11
+ import type { ICERestartResponse } from './signal';
12
+ import type { ICERestartRequest } from './signal';
13
+ import type { UpdateMuteStatesResponse } from './signal';
14
+ import type { UpdateMuteStatesRequest } from './signal';
15
+ import type { UpdateSubscriptionsResponse } from './signal';
16
+ import type { UpdateSubscriptionsRequest } from './signal';
17
+ import type { ICETrickleResponse } from './signal';
3
18
  import type { ICETrickle } from '../models/models';
19
+ import type { SendAnswerResponse } from './signal';
20
+ import type { SendAnswerRequest } from './signal';
21
+ import type { SetPublisherResponse } from './signal';
22
+ import type { SetPublisherRequest } from './signal';
23
+ import type { UnaryCall } from '@protobuf-ts/runtime-rpc';
24
+ import type { RpcOptions } from '@protobuf-ts/runtime-rpc';
4
25
  /**
5
26
  * @generated from protobuf service stream.video.sfu.signal.SignalServer
6
27
  */
@@ -82,5 +82,6 @@ export declare class ClientEventReporter {
82
82
  private sessionIdField;
83
83
  private buildCommon;
84
84
  private send;
85
+ private sendForCall;
85
86
  private sendWithRetry;
86
87
  }
@@ -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
- private onReconnectionNeeded?;
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 { ReplaySubject } from 'rxjs';
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 subscriberCandidates: ReplaySubject<RTCIceCandidateInit>;
9
- readonly publisherCandidates: ReplaySubject<RTCIceCandidateInit>;
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
  *
@@ -10,10 +10,13 @@ export declare class Publisher extends BasePeerConnection {
10
10
  private readonly transceiverCache;
11
11
  private readonly clonedTracks;
12
12
  private publishOptions;
13
+ private readonly selfSubEnabled;
13
14
  /**
14
15
  * Constructs a new `Publisher` instance.
15
16
  */
16
- constructor(baseOptions: BasePeerConnectionOpts, publishOptions: PublishOption[]);
17
+ constructor(baseOptions: BasePeerConnectionOpts, publishOptions: PublishOption[], opts?: {
18
+ selfSubEnabled?: boolean;
19
+ });
17
20
  /**
18
21
  * Disposes this Publisher instance.
19
22
  */
@@ -7,6 +7,14 @@ import { BasePeerConnectionOpts } from './types';
7
7
  * @internal
8
8
  */
9
9
  export declare class Subscriber extends BasePeerConnection {
10
+ /**
11
+ * Remote streams received from the SFU. For a self-sub case
12
+ * we need to be able to distinguish between the local capture stream.
13
+ * The map will never contain local streams so we can safely use it to
14
+ * check if the stream is remote and dispose it when needed.
15
+ */
16
+ private trackedStreams?;
17
+ private negotiationFailures;
10
18
  /**
11
19
  * Constructs a new `Subscriber` instance.
12
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;
@@ -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
- flush: () => void;
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 previousStats;
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
- * Get the stats from the RTCPeerConnection.
26
- * When called, it will return the stats for the current connection.
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
- get: () => Promise<ComputedStats>;
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
- private readonly id;
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.54.0",
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.8.2",
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": "^5.9.3",
62
+ "typescript": "^6.0.3",
61
63
  "vitest": "^4.1.7",
62
64
  "vitest-mock-extended": "^4.0.0"
63
65
  }