@furious.luke/argus-js 0.5.5 → 0.5.6
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/README.md +53 -0
- package/dist/index.cjs +293 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +124 -1
- package/dist/index.d.ts +124 -1
- package/dist/index.js +293 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -131,6 +131,19 @@ interface PublisherCallbacks {
|
|
|
131
131
|
onUserTextResult?: (event: UserTextResultEvent) => void;
|
|
132
132
|
/** Called once the explicitly requested outbound speech track arrives. */
|
|
133
133
|
onSpeechTrack?: (track: MediaStreamTrack, streams: readonly MediaStream[]) => void;
|
|
134
|
+
/**
|
|
135
|
+
* Called for every periodic connection-stats sample while connected — the raw
|
|
136
|
+
* feed behind {@link onConnectionQualityChange}. Fires at
|
|
137
|
+
* {@link PublisherOptions.connectionStatsIntervalMs}.
|
|
138
|
+
*/
|
|
139
|
+
onConnectionStats?: (sample: ConnectionStatsSample) => void;
|
|
140
|
+
/**
|
|
141
|
+
* Called when the derived connection-quality level changes (including the
|
|
142
|
+
* first assessment after connecting). Downgrades are debounced; upgrades fire
|
|
143
|
+
* immediately. Use this to react to a degrading network — e.g. warn the user
|
|
144
|
+
* or drop a secondary track.
|
|
145
|
+
*/
|
|
146
|
+
onConnectionQualityChange?: (quality: ConnectionQuality) => void;
|
|
134
147
|
}
|
|
135
148
|
interface AssistantTextEvent {
|
|
136
149
|
utteranceId: string;
|
|
@@ -144,6 +157,86 @@ interface UserTextResultEvent {
|
|
|
144
157
|
accepted: boolean;
|
|
145
158
|
reason?: string;
|
|
146
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* A coarse assessment of the uplink to the media server, derived from periodic
|
|
162
|
+
* WebRTC stats. Ordered worst-last by severity: `"good"` is a healthy
|
|
163
|
+
* connection, `"critical"` is barely usable.
|
|
164
|
+
*/
|
|
165
|
+
type ConnectionQualityLevel = "good" | "fair" | "poor" | "critical";
|
|
166
|
+
/**
|
|
167
|
+
* One periodic sample of the outbound connection's health, taken from
|
|
168
|
+
* `RTCPeerConnection.getStats()`. Packet loss and send bitrate are windowed over
|
|
169
|
+
* the interval since the previous sample (the first sample covers the session so
|
|
170
|
+
* far); the rest are point-in-time readings. A field is `null` when the browser
|
|
171
|
+
* did not report the underlying stat for this sample.
|
|
172
|
+
*/
|
|
173
|
+
interface ConnectionStatsSample {
|
|
174
|
+
/** Monotonic clock timestamp (ms) when the sample was taken. */
|
|
175
|
+
timestamp: number;
|
|
176
|
+
/**
|
|
177
|
+
* Fraction of outbound RTP packets lost, in `0..1` — the primary quality
|
|
178
|
+
* signal. A traffic-weighted mean of each stream's loss fraction: a stream uses
|
|
179
|
+
* its remote `fractionLost` (its loss ratio over the last RTCP report interval)
|
|
180
|
+
* when present, otherwise its windowed
|
|
181
|
+
* `Δpacketslost / Δ(packetsSent - retransmittedPacketsSent)`. Both kinds of
|
|
182
|
+
* stream are combined, so a lossy stream is never dropped for lacking
|
|
183
|
+
* `fractionLost`.
|
|
184
|
+
*/
|
|
185
|
+
lossRatio: number;
|
|
186
|
+
/** Round-trip time in milliseconds, or `null` if unknown. */
|
|
187
|
+
rttMs: number | null;
|
|
188
|
+
/** Inter-arrival jitter in milliseconds, or `null` if unknown. */
|
|
189
|
+
jitterMs: number | null;
|
|
190
|
+
/** ICE-estimated available outgoing bitrate in bits/sec, or `null` if unknown. */
|
|
191
|
+
availableOutgoingBitrate: number | null;
|
|
192
|
+
/** Actual send bitrate over the interval in bits/sec, or `null` if unknown. */
|
|
193
|
+
sendBitrate: number | null;
|
|
194
|
+
/**
|
|
195
|
+
* Why the encoder is limiting quality: `"none"`, `"cpu"`, `"bandwidth"`, or
|
|
196
|
+
* `"other"`. `null` when no video is being sent. `"bandwidth"` is treated as a
|
|
197
|
+
* degradation signal by the default classifier.
|
|
198
|
+
*/
|
|
199
|
+
qualityLimitationReason: string | null;
|
|
200
|
+
/** Cumulative NACKs received on outbound media, or `null` if unknown. */
|
|
201
|
+
nackCount: number | null;
|
|
202
|
+
/** Cumulative PLIs received on outbound media, or `null` if unknown. */
|
|
203
|
+
pliCount: number | null;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* A committed connection-quality assessment, delivered to
|
|
207
|
+
* {@link PublisherCallbacks.onConnectionQualityChange} whenever the level
|
|
208
|
+
* changes. `sample` is the reading that triggered the transition.
|
|
209
|
+
*/
|
|
210
|
+
interface ConnectionQuality {
|
|
211
|
+
level: ConnectionQualityLevel;
|
|
212
|
+
sample: ConnectionStatsSample;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Thresholds mapping a {@link ConnectionStatsSample} to a
|
|
216
|
+
* {@link ConnectionQualityLevel}. Packet loss is the primary axis; RTT and
|
|
217
|
+
* jitter can only push the assessment to a worse level, never a better one, and
|
|
218
|
+
* a `"bandwidth"` encoder limitation forces at least `"fair"`. Any subset may be
|
|
219
|
+
* overridden via {@link PublisherOptions.connectionQualityThresholds}; the rest
|
|
220
|
+
* keep their defaults.
|
|
221
|
+
*/
|
|
222
|
+
interface ConnectionQualityThresholds {
|
|
223
|
+
/** Loss ratio (`0..1`) at or above which quality is at least `"fair"`. Default `0.02`. */
|
|
224
|
+
fairLossRatio: number;
|
|
225
|
+
/** Loss ratio at or above which quality is at least `"poor"`. Default `0.05`. */
|
|
226
|
+
poorLossRatio: number;
|
|
227
|
+
/** Loss ratio at or above which quality is `"critical"`. Default `0.12`. */
|
|
228
|
+
criticalLossRatio: number;
|
|
229
|
+
/** RTT (ms) at or above which quality is at least `"fair"`. Default `300`. */
|
|
230
|
+
fairRttMs: number;
|
|
231
|
+
/** RTT (ms) at or above which quality is at least `"poor"`. Default `600`. */
|
|
232
|
+
poorRttMs: number;
|
|
233
|
+
/** RTT (ms) at or above which quality is `"critical"`. Default `1000`. */
|
|
234
|
+
criticalRttMs: number;
|
|
235
|
+
/** Jitter (ms) at or above which quality is at least `"fair"`. Default `50`. */
|
|
236
|
+
fairJitterMs: number;
|
|
237
|
+
/** Jitter (ms) at or above which quality is at least `"poor"`. Default `150`. */
|
|
238
|
+
poorJitterMs: number;
|
|
239
|
+
}
|
|
147
240
|
type PublisherRecoveryState = "recovering" | "recovered" | "failed";
|
|
148
241
|
type PublisherRecoveryAction = "sender_restart" | "ice_restart";
|
|
149
242
|
type PublisherRecoveryFailureReason = "capture_ended" | "automatic_recovery_failed";
|
|
@@ -232,6 +325,24 @@ interface PublisherOptions {
|
|
|
232
325
|
preferredVideoCodecs?: string[];
|
|
233
326
|
/** How long to retry a dropped signaling connection to the selected gateway. Defaults to 20 seconds. */
|
|
234
327
|
signalingReconnectTimeoutMs?: number;
|
|
328
|
+
/**
|
|
329
|
+
* How often, in milliseconds, to poll `RTCPeerConnection.getStats()` for
|
|
330
|
+
* connection-quality assessment once connected. Defaults to 2000. Set to 0 to
|
|
331
|
+
* disable stats polling entirely — neither {@link PublisherCallbacks.onConnectionStats}
|
|
332
|
+
* nor {@link PublisherCallbacks.onConnectionQualityChange} will fire.
|
|
333
|
+
*/
|
|
334
|
+
connectionStatsIntervalMs?: number;
|
|
335
|
+
/**
|
|
336
|
+
* Overrides for the connection-quality classification thresholds. Any omitted
|
|
337
|
+
* field keeps its default. See {@link ConnectionQualityThresholds}.
|
|
338
|
+
*/
|
|
339
|
+
connectionQualityThresholds?: Partial<ConnectionQualityThresholds>;
|
|
340
|
+
/**
|
|
341
|
+
* Number of consecutive worse-than-current samples required before a quality
|
|
342
|
+
* downgrade is committed, damping transient blips. Improvements are reported
|
|
343
|
+
* on the first better sample. Defaults to 2; values below 1 are treated as 1.
|
|
344
|
+
*/
|
|
345
|
+
connectionQualityDebounceSamples?: number;
|
|
235
346
|
/** Callbacks for lifecycle events. */
|
|
236
347
|
callbacks?: PublisherCallbacks;
|
|
237
348
|
}
|
|
@@ -277,6 +388,11 @@ declare class Publisher {
|
|
|
277
388
|
private gatewayURL;
|
|
278
389
|
private lastReportedICEPath;
|
|
279
390
|
private watchedICETransports;
|
|
391
|
+
private connectionStatsTimer;
|
|
392
|
+
private statsSampleInFlight;
|
|
393
|
+
private lastStatsSample;
|
|
394
|
+
private currentQualityLevel;
|
|
395
|
+
private qualityDowngradeStreak;
|
|
280
396
|
private stopped;
|
|
281
397
|
private lifecycleGeneration;
|
|
282
398
|
private runAbort;
|
|
@@ -459,6 +575,13 @@ declare class Publisher {
|
|
|
459
575
|
private releaseLocalCandidateBatch;
|
|
460
576
|
private applyAnswered;
|
|
461
577
|
private watchSelectedICEPairChanges;
|
|
578
|
+
private startConnectionStatsLoop;
|
|
579
|
+
private stopConnectionStatsLoop;
|
|
580
|
+
private sampleConnectionQuality;
|
|
581
|
+
private buildStatsSample;
|
|
582
|
+
private resolveQualityThresholds;
|
|
583
|
+
private classifyQuality;
|
|
584
|
+
private updateConnectionQuality;
|
|
462
585
|
private reportSelectedICEPath;
|
|
463
586
|
private completeMediaRecovery;
|
|
464
587
|
private failMediaRecovery;
|
|
@@ -688,4 +811,4 @@ interface CaptureMicrophoneOptions {
|
|
|
688
811
|
*/
|
|
689
812
|
declare function captureMicrophone(opts?: CaptureMicrophoneOptions): Promise<MediaStream>;
|
|
690
813
|
|
|
691
|
-
export { type AssistantTextEvent, type CaptureCameraOptions, type CaptureMicrophoneOptions, type CaptureScreenOptions, type GatewayReadyInfo, Publisher, type PublisherCallbacks, type PublisherOptions, type PublisherRecoveryAction, type PublisherRecoveryEvent, type PublisherRecoveryFailureReason, type PublisherRecoveryState, type SignalMessage, type TrackLabel, type TrackType, type TurnTransportPolicy, type UserTextResultEvent, type VideoTrackType, captureCamera, captureMicrophone, captureScreen };
|
|
814
|
+
export { type AssistantTextEvent, type CaptureCameraOptions, type CaptureMicrophoneOptions, type CaptureScreenOptions, type ConnectionQuality, type ConnectionQualityLevel, type ConnectionQualityThresholds, type ConnectionStatsSample, type GatewayReadyInfo, Publisher, type PublisherCallbacks, type PublisherOptions, type PublisherRecoveryAction, type PublisherRecoveryEvent, type PublisherRecoveryFailureReason, type PublisherRecoveryState, type SignalMessage, type TrackLabel, type TrackType, type TurnTransportPolicy, type UserTextResultEvent, type VideoTrackType, captureCamera, captureMicrophone, captureScreen };
|
package/dist/index.d.ts
CHANGED
|
@@ -131,6 +131,19 @@ interface PublisherCallbacks {
|
|
|
131
131
|
onUserTextResult?: (event: UserTextResultEvent) => void;
|
|
132
132
|
/** Called once the explicitly requested outbound speech track arrives. */
|
|
133
133
|
onSpeechTrack?: (track: MediaStreamTrack, streams: readonly MediaStream[]) => void;
|
|
134
|
+
/**
|
|
135
|
+
* Called for every periodic connection-stats sample while connected — the raw
|
|
136
|
+
* feed behind {@link onConnectionQualityChange}. Fires at
|
|
137
|
+
* {@link PublisherOptions.connectionStatsIntervalMs}.
|
|
138
|
+
*/
|
|
139
|
+
onConnectionStats?: (sample: ConnectionStatsSample) => void;
|
|
140
|
+
/**
|
|
141
|
+
* Called when the derived connection-quality level changes (including the
|
|
142
|
+
* first assessment after connecting). Downgrades are debounced; upgrades fire
|
|
143
|
+
* immediately. Use this to react to a degrading network — e.g. warn the user
|
|
144
|
+
* or drop a secondary track.
|
|
145
|
+
*/
|
|
146
|
+
onConnectionQualityChange?: (quality: ConnectionQuality) => void;
|
|
134
147
|
}
|
|
135
148
|
interface AssistantTextEvent {
|
|
136
149
|
utteranceId: string;
|
|
@@ -144,6 +157,86 @@ interface UserTextResultEvent {
|
|
|
144
157
|
accepted: boolean;
|
|
145
158
|
reason?: string;
|
|
146
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* A coarse assessment of the uplink to the media server, derived from periodic
|
|
162
|
+
* WebRTC stats. Ordered worst-last by severity: `"good"` is a healthy
|
|
163
|
+
* connection, `"critical"` is barely usable.
|
|
164
|
+
*/
|
|
165
|
+
type ConnectionQualityLevel = "good" | "fair" | "poor" | "critical";
|
|
166
|
+
/**
|
|
167
|
+
* One periodic sample of the outbound connection's health, taken from
|
|
168
|
+
* `RTCPeerConnection.getStats()`. Packet loss and send bitrate are windowed over
|
|
169
|
+
* the interval since the previous sample (the first sample covers the session so
|
|
170
|
+
* far); the rest are point-in-time readings. A field is `null` when the browser
|
|
171
|
+
* did not report the underlying stat for this sample.
|
|
172
|
+
*/
|
|
173
|
+
interface ConnectionStatsSample {
|
|
174
|
+
/** Monotonic clock timestamp (ms) when the sample was taken. */
|
|
175
|
+
timestamp: number;
|
|
176
|
+
/**
|
|
177
|
+
* Fraction of outbound RTP packets lost, in `0..1` — the primary quality
|
|
178
|
+
* signal. A traffic-weighted mean of each stream's loss fraction: a stream uses
|
|
179
|
+
* its remote `fractionLost` (its loss ratio over the last RTCP report interval)
|
|
180
|
+
* when present, otherwise its windowed
|
|
181
|
+
* `Δpacketslost / Δ(packetsSent - retransmittedPacketsSent)`. Both kinds of
|
|
182
|
+
* stream are combined, so a lossy stream is never dropped for lacking
|
|
183
|
+
* `fractionLost`.
|
|
184
|
+
*/
|
|
185
|
+
lossRatio: number;
|
|
186
|
+
/** Round-trip time in milliseconds, or `null` if unknown. */
|
|
187
|
+
rttMs: number | null;
|
|
188
|
+
/** Inter-arrival jitter in milliseconds, or `null` if unknown. */
|
|
189
|
+
jitterMs: number | null;
|
|
190
|
+
/** ICE-estimated available outgoing bitrate in bits/sec, or `null` if unknown. */
|
|
191
|
+
availableOutgoingBitrate: number | null;
|
|
192
|
+
/** Actual send bitrate over the interval in bits/sec, or `null` if unknown. */
|
|
193
|
+
sendBitrate: number | null;
|
|
194
|
+
/**
|
|
195
|
+
* Why the encoder is limiting quality: `"none"`, `"cpu"`, `"bandwidth"`, or
|
|
196
|
+
* `"other"`. `null` when no video is being sent. `"bandwidth"` is treated as a
|
|
197
|
+
* degradation signal by the default classifier.
|
|
198
|
+
*/
|
|
199
|
+
qualityLimitationReason: string | null;
|
|
200
|
+
/** Cumulative NACKs received on outbound media, or `null` if unknown. */
|
|
201
|
+
nackCount: number | null;
|
|
202
|
+
/** Cumulative PLIs received on outbound media, or `null` if unknown. */
|
|
203
|
+
pliCount: number | null;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* A committed connection-quality assessment, delivered to
|
|
207
|
+
* {@link PublisherCallbacks.onConnectionQualityChange} whenever the level
|
|
208
|
+
* changes. `sample` is the reading that triggered the transition.
|
|
209
|
+
*/
|
|
210
|
+
interface ConnectionQuality {
|
|
211
|
+
level: ConnectionQualityLevel;
|
|
212
|
+
sample: ConnectionStatsSample;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Thresholds mapping a {@link ConnectionStatsSample} to a
|
|
216
|
+
* {@link ConnectionQualityLevel}. Packet loss is the primary axis; RTT and
|
|
217
|
+
* jitter can only push the assessment to a worse level, never a better one, and
|
|
218
|
+
* a `"bandwidth"` encoder limitation forces at least `"fair"`. Any subset may be
|
|
219
|
+
* overridden via {@link PublisherOptions.connectionQualityThresholds}; the rest
|
|
220
|
+
* keep their defaults.
|
|
221
|
+
*/
|
|
222
|
+
interface ConnectionQualityThresholds {
|
|
223
|
+
/** Loss ratio (`0..1`) at or above which quality is at least `"fair"`. Default `0.02`. */
|
|
224
|
+
fairLossRatio: number;
|
|
225
|
+
/** Loss ratio at or above which quality is at least `"poor"`. Default `0.05`. */
|
|
226
|
+
poorLossRatio: number;
|
|
227
|
+
/** Loss ratio at or above which quality is `"critical"`. Default `0.12`. */
|
|
228
|
+
criticalLossRatio: number;
|
|
229
|
+
/** RTT (ms) at or above which quality is at least `"fair"`. Default `300`. */
|
|
230
|
+
fairRttMs: number;
|
|
231
|
+
/** RTT (ms) at or above which quality is at least `"poor"`. Default `600`. */
|
|
232
|
+
poorRttMs: number;
|
|
233
|
+
/** RTT (ms) at or above which quality is `"critical"`. Default `1000`. */
|
|
234
|
+
criticalRttMs: number;
|
|
235
|
+
/** Jitter (ms) at or above which quality is at least `"fair"`. Default `50`. */
|
|
236
|
+
fairJitterMs: number;
|
|
237
|
+
/** Jitter (ms) at or above which quality is at least `"poor"`. Default `150`. */
|
|
238
|
+
poorJitterMs: number;
|
|
239
|
+
}
|
|
147
240
|
type PublisherRecoveryState = "recovering" | "recovered" | "failed";
|
|
148
241
|
type PublisherRecoveryAction = "sender_restart" | "ice_restart";
|
|
149
242
|
type PublisherRecoveryFailureReason = "capture_ended" | "automatic_recovery_failed";
|
|
@@ -232,6 +325,24 @@ interface PublisherOptions {
|
|
|
232
325
|
preferredVideoCodecs?: string[];
|
|
233
326
|
/** How long to retry a dropped signaling connection to the selected gateway. Defaults to 20 seconds. */
|
|
234
327
|
signalingReconnectTimeoutMs?: number;
|
|
328
|
+
/**
|
|
329
|
+
* How often, in milliseconds, to poll `RTCPeerConnection.getStats()` for
|
|
330
|
+
* connection-quality assessment once connected. Defaults to 2000. Set to 0 to
|
|
331
|
+
* disable stats polling entirely — neither {@link PublisherCallbacks.onConnectionStats}
|
|
332
|
+
* nor {@link PublisherCallbacks.onConnectionQualityChange} will fire.
|
|
333
|
+
*/
|
|
334
|
+
connectionStatsIntervalMs?: number;
|
|
335
|
+
/**
|
|
336
|
+
* Overrides for the connection-quality classification thresholds. Any omitted
|
|
337
|
+
* field keeps its default. See {@link ConnectionQualityThresholds}.
|
|
338
|
+
*/
|
|
339
|
+
connectionQualityThresholds?: Partial<ConnectionQualityThresholds>;
|
|
340
|
+
/**
|
|
341
|
+
* Number of consecutive worse-than-current samples required before a quality
|
|
342
|
+
* downgrade is committed, damping transient blips. Improvements are reported
|
|
343
|
+
* on the first better sample. Defaults to 2; values below 1 are treated as 1.
|
|
344
|
+
*/
|
|
345
|
+
connectionQualityDebounceSamples?: number;
|
|
235
346
|
/** Callbacks for lifecycle events. */
|
|
236
347
|
callbacks?: PublisherCallbacks;
|
|
237
348
|
}
|
|
@@ -277,6 +388,11 @@ declare class Publisher {
|
|
|
277
388
|
private gatewayURL;
|
|
278
389
|
private lastReportedICEPath;
|
|
279
390
|
private watchedICETransports;
|
|
391
|
+
private connectionStatsTimer;
|
|
392
|
+
private statsSampleInFlight;
|
|
393
|
+
private lastStatsSample;
|
|
394
|
+
private currentQualityLevel;
|
|
395
|
+
private qualityDowngradeStreak;
|
|
280
396
|
private stopped;
|
|
281
397
|
private lifecycleGeneration;
|
|
282
398
|
private runAbort;
|
|
@@ -459,6 +575,13 @@ declare class Publisher {
|
|
|
459
575
|
private releaseLocalCandidateBatch;
|
|
460
576
|
private applyAnswered;
|
|
461
577
|
private watchSelectedICEPairChanges;
|
|
578
|
+
private startConnectionStatsLoop;
|
|
579
|
+
private stopConnectionStatsLoop;
|
|
580
|
+
private sampleConnectionQuality;
|
|
581
|
+
private buildStatsSample;
|
|
582
|
+
private resolveQualityThresholds;
|
|
583
|
+
private classifyQuality;
|
|
584
|
+
private updateConnectionQuality;
|
|
462
585
|
private reportSelectedICEPath;
|
|
463
586
|
private completeMediaRecovery;
|
|
464
587
|
private failMediaRecovery;
|
|
@@ -688,4 +811,4 @@ interface CaptureMicrophoneOptions {
|
|
|
688
811
|
*/
|
|
689
812
|
declare function captureMicrophone(opts?: CaptureMicrophoneOptions): Promise<MediaStream>;
|
|
690
813
|
|
|
691
|
-
export { type AssistantTextEvent, type CaptureCameraOptions, type CaptureMicrophoneOptions, type CaptureScreenOptions, type GatewayReadyInfo, Publisher, type PublisherCallbacks, type PublisherOptions, type PublisherRecoveryAction, type PublisherRecoveryEvent, type PublisherRecoveryFailureReason, type PublisherRecoveryState, type SignalMessage, type TrackLabel, type TrackType, type TurnTransportPolicy, type UserTextResultEvent, type VideoTrackType, captureCamera, captureMicrophone, captureScreen };
|
|
814
|
+
export { type AssistantTextEvent, type CaptureCameraOptions, type CaptureMicrophoneOptions, type CaptureScreenOptions, type ConnectionQuality, type ConnectionQualityLevel, type ConnectionQualityThresholds, type ConnectionStatsSample, type GatewayReadyInfo, Publisher, type PublisherCallbacks, type PublisherOptions, type PublisherRecoveryAction, type PublisherRecoveryEvent, type PublisherRecoveryFailureReason, type PublisherRecoveryState, type SignalMessage, type TrackLabel, type TrackType, type TurnTransportPolicy, type UserTextResultEvent, type VideoTrackType, captureCamera, captureMicrophone, captureScreen };
|
package/dist/index.js
CHANGED
|
@@ -65,6 +65,28 @@ function selectGatewayTURNURLs(advertised, policy = "all") {
|
|
|
65
65
|
}
|
|
66
66
|
return selected;
|
|
67
67
|
}
|
|
68
|
+
var qualityLevels = ["good", "fair", "poor", "critical"];
|
|
69
|
+
function qualitySeverity(level) {
|
|
70
|
+
return qualityLevels.indexOf(level);
|
|
71
|
+
}
|
|
72
|
+
function nowMs() {
|
|
73
|
+
return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
|
|
74
|
+
}
|
|
75
|
+
var limitationSeverity = { none: 0, other: 1, cpu: 2, bandwidth: 3 };
|
|
76
|
+
function worseLimitation(current, next) {
|
|
77
|
+
if (current === null) return next;
|
|
78
|
+
return (limitationSeverity[next] ?? 1) > (limitationSeverity[current] ?? 1) ? next : current;
|
|
79
|
+
}
|
|
80
|
+
function findNominatedCandidatePair(stats) {
|
|
81
|
+
let found;
|
|
82
|
+
stats.forEach((report) => {
|
|
83
|
+
const value = report;
|
|
84
|
+
if (!found && value.type === "candidate-pair" && value.state === "succeeded" && value.nominated === true) {
|
|
85
|
+
found = value;
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
return found;
|
|
89
|
+
}
|
|
68
90
|
var defaultSignalingReconnectTimeoutMs = 2e4;
|
|
69
91
|
var defaultGatewayHandshakeTimeoutMs = 2e4;
|
|
70
92
|
var defaultPeerConnectionTimeoutMs = 3e4;
|
|
@@ -85,6 +107,18 @@ var negotiationReconnectGraceMs = 5e3;
|
|
|
85
107
|
var minimumIntentionalTrackEndRetentionMs = 35e3;
|
|
86
108
|
var maxUserTextBytes = 4 * 1024;
|
|
87
109
|
var maxRetainedICECandidates = 64;
|
|
110
|
+
var defaultConnectionStatsIntervalMs = 2e3;
|
|
111
|
+
var defaultConnectionQualityDebounceSamples = 2;
|
|
112
|
+
var defaultConnectionQualityThresholds = {
|
|
113
|
+
fairLossRatio: 0.02,
|
|
114
|
+
poorLossRatio: 0.05,
|
|
115
|
+
criticalLossRatio: 0.12,
|
|
116
|
+
fairRttMs: 300,
|
|
117
|
+
poorRttMs: 600,
|
|
118
|
+
criticalRttMs: 1e3,
|
|
119
|
+
fairJitterMs: 50,
|
|
120
|
+
poorJitterMs: 150
|
|
121
|
+
};
|
|
88
122
|
var ReportedPublisherError = class extends Error {
|
|
89
123
|
constructor(message, fatal = false) {
|
|
90
124
|
super(message);
|
|
@@ -127,6 +161,17 @@ var Publisher = class {
|
|
|
127
161
|
gatewayURL = null;
|
|
128
162
|
lastReportedICEPath = null;
|
|
129
163
|
watchedICETransports = /* @__PURE__ */ new WeakSet();
|
|
164
|
+
// Periodic getStats() polling for connection-quality assessment. The timer runs
|
|
165
|
+
// only while connected; lastStatsSample anchors the windowed loss/bitrate deltas
|
|
166
|
+
// (loss is tracked per outbound SSRC so it can be paired to the remote's report),
|
|
167
|
+
// and currentQualityLevel/qualityDowngradeStreak drive the debounced classifier.
|
|
168
|
+
connectionStatsTimer = null;
|
|
169
|
+
// Guards against overlapping getStats() calls: if one outlives the interval,
|
|
170
|
+
// ticks are skipped until it resolves so samples never complete out of order.
|
|
171
|
+
statsSampleInFlight = false;
|
|
172
|
+
lastStatsSample = null;
|
|
173
|
+
currentQualityLevel = null;
|
|
174
|
+
qualityDowngradeStreak = 0;
|
|
130
175
|
stopped = true;
|
|
131
176
|
// Every start/stop boundary advances lifecycleGeneration. Async work captures
|
|
132
177
|
// the generation it belongs to and may never mutate or terminate a later run.
|
|
@@ -339,10 +384,13 @@ var Publisher = class {
|
|
|
339
384
|
if (state === "connected") {
|
|
340
385
|
this.clearPeerConnectionTimeout();
|
|
341
386
|
void this.reportSelectedICEPath(pc);
|
|
387
|
+
this.startConnectionStatsLoop(generation, pc);
|
|
342
388
|
this.opts.callbacks?.onConnected?.();
|
|
343
389
|
} else if (state === "failed") {
|
|
344
390
|
this.clearPeerConnectionTimeout();
|
|
345
391
|
this.terminateWithError(new Error("WebRTC connection failed"), true, generation);
|
|
392
|
+
} else {
|
|
393
|
+
this.stopConnectionStatsLoop();
|
|
346
394
|
}
|
|
347
395
|
};
|
|
348
396
|
if (initialTrack) {
|
|
@@ -477,6 +525,7 @@ var Publisher = class {
|
|
|
477
525
|
this.runAbort?.abort();
|
|
478
526
|
this.runAbort = null;
|
|
479
527
|
this.clearPeerConnectionTimeout();
|
|
528
|
+
this.stopConnectionStatsLoop();
|
|
480
529
|
this.stopped = true;
|
|
481
530
|
this.cancelAllMediaRecovery();
|
|
482
531
|
this.reconnectGeneration++;
|
|
@@ -965,6 +1014,7 @@ var Publisher = class {
|
|
|
965
1014
|
this.runAbort?.abort();
|
|
966
1015
|
this.runAbort = null;
|
|
967
1016
|
this.clearPeerConnectionTimeout();
|
|
1017
|
+
this.stopConnectionStatsLoop();
|
|
968
1018
|
this.stopped = true;
|
|
969
1019
|
this.cancelAllMediaRecovery();
|
|
970
1020
|
this.reconnectGeneration++;
|
|
@@ -1435,6 +1485,249 @@ var Publisher = class {
|
|
|
1435
1485
|
} catch {
|
|
1436
1486
|
}
|
|
1437
1487
|
}
|
|
1488
|
+
// startConnectionStatsLoop begins periodic getStats() sampling once the peer
|
|
1489
|
+
// connection is connected. It is a no-op when polling is disabled or no
|
|
1490
|
+
// consumer is listening, and it re-baselines on each call so a reconnect after
|
|
1491
|
+
// an ICE restart starts a fresh quality assessment.
|
|
1492
|
+
startConnectionStatsLoop(generation, pc) {
|
|
1493
|
+
const intervalMs = Math.max(
|
|
1494
|
+
0,
|
|
1495
|
+
this.opts.connectionStatsIntervalMs ?? defaultConnectionStatsIntervalMs
|
|
1496
|
+
);
|
|
1497
|
+
const listening = !!this.opts.callbacks?.onConnectionStats || !!this.opts.callbacks?.onConnectionQualityChange;
|
|
1498
|
+
if (intervalMs === 0 || !listening) return;
|
|
1499
|
+
this.stopConnectionStatsLoop();
|
|
1500
|
+
const timer = setInterval(() => {
|
|
1501
|
+
if (this.connectionStatsTimer !== timer) return;
|
|
1502
|
+
if (!this.isActiveRun(generation, pc)) {
|
|
1503
|
+
this.stopConnectionStatsLoop();
|
|
1504
|
+
return;
|
|
1505
|
+
}
|
|
1506
|
+
if (this.statsSampleInFlight) return;
|
|
1507
|
+
this.statsSampleInFlight = true;
|
|
1508
|
+
void this.sampleConnectionQuality(generation, pc, timer);
|
|
1509
|
+
}, intervalMs);
|
|
1510
|
+
this.connectionStatsTimer = timer;
|
|
1511
|
+
}
|
|
1512
|
+
stopConnectionStatsLoop() {
|
|
1513
|
+
if (this.connectionStatsTimer !== null) clearInterval(this.connectionStatsTimer);
|
|
1514
|
+
this.connectionStatsTimer = null;
|
|
1515
|
+
this.statsSampleInFlight = false;
|
|
1516
|
+
this.lastStatsSample = null;
|
|
1517
|
+
this.currentQualityLevel = null;
|
|
1518
|
+
this.qualityDowngradeStreak = 0;
|
|
1519
|
+
}
|
|
1520
|
+
async sampleConnectionQuality(generation, pc, timer) {
|
|
1521
|
+
let stats = null;
|
|
1522
|
+
try {
|
|
1523
|
+
stats = await pc.getStats();
|
|
1524
|
+
} catch {
|
|
1525
|
+
}
|
|
1526
|
+
if (this.connectionStatsTimer !== timer) return;
|
|
1527
|
+
this.statsSampleInFlight = false;
|
|
1528
|
+
if (!stats || !this.isActiveRun(generation, pc) || pc.connectionState !== "connected") {
|
|
1529
|
+
return;
|
|
1530
|
+
}
|
|
1531
|
+
const sample = this.buildStatsSample(stats);
|
|
1532
|
+
this.opts.callbacks?.onConnectionStats?.(sample);
|
|
1533
|
+
if (this.connectionStatsTimer !== timer || !this.isActiveRun(generation, pc) || pc.connectionState !== "connected") {
|
|
1534
|
+
return;
|
|
1535
|
+
}
|
|
1536
|
+
this.updateConnectionQuality(sample);
|
|
1537
|
+
}
|
|
1538
|
+
// buildStatsSample derives one sample from a getStats() report. Loss is a mean of
|
|
1539
|
+
// per-stream loss fractions weighted by each stream's packets sent this window, so
|
|
1540
|
+
// every stream carrying traffic contributes. A stream uses its remote fractionLost
|
|
1541
|
+
// when present — the remote's loss ratio over its RR interval, self-aligned and
|
|
1542
|
+
// excluding retransmissions (which ride a separate SSRC); dividing the remote's
|
|
1543
|
+
// Δ(packetsLost) by the local Δ(packetsSent) would instead misalign an RTCP-timed
|
|
1544
|
+
// numerator with a continuously-updated denominator — and otherwise falls back to
|
|
1545
|
+
// its own windowed Δ(packetsLost) / Δ(distinct media packets sent), a denominator
|
|
1546
|
+
// that excludes retransmissions (packetsSent - retransmittedPacketsSent). Mixing the
|
|
1547
|
+
// two per stream keeps a lossy stream from being dropped when a sibling has fractionLost.
|
|
1548
|
+
//
|
|
1549
|
+
// Deltas are taken only over outbound stats-object ids present in BOTH this and the
|
|
1550
|
+
// previous sample. A track replace/unpublish (or a recycled SSRC) deletes the old
|
|
1551
|
+
// stats object and creates a new one with a new id; counting a vanished stream's
|
|
1552
|
+
// missing tail, or a fresh object's cumulative total as an interval delta, would
|
|
1553
|
+
// corrupt loss. Excluding the symmetric difference baselines new objects (they count
|
|
1554
|
+
// from their next sample) and drops departed ones, so continuous streams still measure
|
|
1555
|
+
// loss even when an SSRC number is reused across distinct stats objects.
|
|
1556
|
+
buildStatsSample(stats) {
|
|
1557
|
+
const timestamp = nowMs();
|
|
1558
|
+
let nackCount = 0;
|
|
1559
|
+
let pliCount = 0;
|
|
1560
|
+
let haveOutbound = false;
|
|
1561
|
+
let haveOutboundVideo = false;
|
|
1562
|
+
let rttSeconds = null;
|
|
1563
|
+
let jitterSeconds = null;
|
|
1564
|
+
let limitationReason = null;
|
|
1565
|
+
let selectedPairID;
|
|
1566
|
+
let transportBytes = null;
|
|
1567
|
+
const streams = /* @__PURE__ */ new Map();
|
|
1568
|
+
const ssrcToId = /* @__PURE__ */ new Map();
|
|
1569
|
+
stats.forEach((report) => {
|
|
1570
|
+
const value = report;
|
|
1571
|
+
switch (value.type) {
|
|
1572
|
+
case "outbound-rtp": {
|
|
1573
|
+
haveOutbound = true;
|
|
1574
|
+
if (value.kind === "video") haveOutboundVideo = true;
|
|
1575
|
+
const ssrc = typeof value.ssrc === "number" ? value.ssrc : NaN;
|
|
1576
|
+
const sent = typeof value.packetsSent === "number" ? value.packetsSent : 0;
|
|
1577
|
+
const retransmitted = typeof value.retransmittedPacketsSent === "number" ? value.retransmittedPacketsSent : 0;
|
|
1578
|
+
const bytesSent = typeof value.bytesSent === "number" ? value.bytesSent : 0;
|
|
1579
|
+
if (typeof value.nackCount === "number") nackCount += value.nackCount;
|
|
1580
|
+
if (typeof value.pliCount === "number") pliCount += value.pliCount;
|
|
1581
|
+
if (typeof value.qualityLimitationReason === "string") {
|
|
1582
|
+
limitationReason = worseLimitation(limitationReason, value.qualityLimitationReason);
|
|
1583
|
+
}
|
|
1584
|
+
if (typeof value.id === "string") {
|
|
1585
|
+
const expected = Math.max(0, sent - retransmitted);
|
|
1586
|
+
streams.set(value.id, { expected, lost: null, fractionLost: null, bytesSent });
|
|
1587
|
+
if (!Number.isNaN(ssrc)) ssrcToId.set(ssrc, value.id);
|
|
1588
|
+
}
|
|
1589
|
+
break;
|
|
1590
|
+
}
|
|
1591
|
+
case "remote-inbound-rtp": {
|
|
1592
|
+
if (typeof value.roundTripTime === "number") {
|
|
1593
|
+
rttSeconds = Math.max(rttSeconds ?? 0, value.roundTripTime);
|
|
1594
|
+
}
|
|
1595
|
+
if (typeof value.jitter === "number") {
|
|
1596
|
+
jitterSeconds = Math.max(jitterSeconds ?? 0, value.jitter);
|
|
1597
|
+
}
|
|
1598
|
+
break;
|
|
1599
|
+
}
|
|
1600
|
+
case "transport": {
|
|
1601
|
+
if (typeof value.selectedCandidatePairId === "string") {
|
|
1602
|
+
selectedPairID = value.selectedCandidatePairId;
|
|
1603
|
+
}
|
|
1604
|
+
if (typeof value.bytesSent === "number") {
|
|
1605
|
+
transportBytes = (transportBytes ?? 0) + value.bytesSent;
|
|
1606
|
+
}
|
|
1607
|
+
break;
|
|
1608
|
+
}
|
|
1609
|
+
default:
|
|
1610
|
+
break;
|
|
1611
|
+
}
|
|
1612
|
+
});
|
|
1613
|
+
stats.forEach((report) => {
|
|
1614
|
+
const value = report;
|
|
1615
|
+
if (value.type !== "remote-inbound-rtp") return;
|
|
1616
|
+
const outboundId = typeof value.localId === "string" && streams.has(value.localId) ? value.localId : typeof value.ssrc === "number" ? ssrcToId.get(value.ssrc) : void 0;
|
|
1617
|
+
if (outboundId === void 0) return;
|
|
1618
|
+
const stream = streams.get(outboundId);
|
|
1619
|
+
if (!stream) return;
|
|
1620
|
+
const lost = typeof value.packetsLost === "number" ? value.packetsLost : 0;
|
|
1621
|
+
stream.lost = (stream.lost ?? 0) + lost;
|
|
1622
|
+
if (typeof value.fractionLost === "number" && Number.isFinite(value.fractionLost)) {
|
|
1623
|
+
stream.fractionLost = Math.min(1, Math.max(0, value.fractionLost));
|
|
1624
|
+
}
|
|
1625
|
+
});
|
|
1626
|
+
const pair = (selectedPairID ? stats.get(selectedPairID) : void 0) ?? findNominatedCandidatePair(stats);
|
|
1627
|
+
const availableOutgoingBitrate = pair && typeof pair.availableOutgoingBitrate === "number" ? pair.availableOutgoingBitrate : null;
|
|
1628
|
+
if (rttSeconds === null && pair && typeof pair.currentRoundTripTime === "number") {
|
|
1629
|
+
rttSeconds = pair.currentRoundTripTime;
|
|
1630
|
+
}
|
|
1631
|
+
const prev = this.lastStatsSample;
|
|
1632
|
+
let weightedFractionSum = 0;
|
|
1633
|
+
let weightSum = 0;
|
|
1634
|
+
let intersectionBytesDelta = 0;
|
|
1635
|
+
let hadStreamOverlap = false;
|
|
1636
|
+
streams.forEach((current, id) => {
|
|
1637
|
+
const before = prev?.streams.get(id);
|
|
1638
|
+
if (!before) return;
|
|
1639
|
+
hadStreamOverlap = true;
|
|
1640
|
+
intersectionBytesDelta += Math.max(0, current.bytesSent - before.bytesSent);
|
|
1641
|
+
const deltaExpected = Math.max(0, current.expected - before.expected);
|
|
1642
|
+
if (deltaExpected <= 0) return;
|
|
1643
|
+
let fraction = null;
|
|
1644
|
+
if (current.fractionLost !== null) {
|
|
1645
|
+
fraction = current.fractionLost;
|
|
1646
|
+
} else if (current.lost !== null && before.lost !== null) {
|
|
1647
|
+
fraction = Math.min(1, Math.max(0, current.lost - before.lost) / deltaExpected);
|
|
1648
|
+
}
|
|
1649
|
+
if (fraction === null) return;
|
|
1650
|
+
weightedFractionSum += fraction * deltaExpected;
|
|
1651
|
+
weightSum += deltaExpected;
|
|
1652
|
+
});
|
|
1653
|
+
const deltaSeconds = prev ? (timestamp - prev.timestamp) / 1e3 : 0;
|
|
1654
|
+
this.lastStatsSample = { timestamp, transportBytes, streams };
|
|
1655
|
+
const lossRatio = weightSum > 0 ? Math.min(1, weightedFractionSum / weightSum) : 0;
|
|
1656
|
+
let sendBitrate = null;
|
|
1657
|
+
if (prev && deltaSeconds > 0) {
|
|
1658
|
+
if (transportBytes !== null && prev.transportBytes !== null) {
|
|
1659
|
+
sendBitrate = Math.max(0, transportBytes - prev.transportBytes) * 8 / deltaSeconds;
|
|
1660
|
+
} else if (hadStreamOverlap) {
|
|
1661
|
+
sendBitrate = intersectionBytesDelta * 8 / deltaSeconds;
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
return {
|
|
1665
|
+
timestamp,
|
|
1666
|
+
lossRatio,
|
|
1667
|
+
rttMs: rttSeconds !== null ? rttSeconds * 1e3 : null,
|
|
1668
|
+
jitterMs: jitterSeconds !== null ? jitterSeconds * 1e3 : null,
|
|
1669
|
+
availableOutgoingBitrate,
|
|
1670
|
+
sendBitrate,
|
|
1671
|
+
qualityLimitationReason: haveOutboundVideo ? limitationReason ?? "none" : null,
|
|
1672
|
+
nackCount: haveOutbound ? nackCount : null,
|
|
1673
|
+
pliCount: haveOutbound ? pliCount : null
|
|
1674
|
+
};
|
|
1675
|
+
}
|
|
1676
|
+
resolveQualityThresholds() {
|
|
1677
|
+
return { ...defaultConnectionQualityThresholds, ...this.opts.connectionQualityThresholds ?? {} };
|
|
1678
|
+
}
|
|
1679
|
+
// classifyQuality maps a sample to a level. Loss is the primary axis; RTT,
|
|
1680
|
+
// jitter, and a bandwidth-limited encoder can only raise severity.
|
|
1681
|
+
classifyQuality(sample) {
|
|
1682
|
+
const t = this.resolveQualityThresholds();
|
|
1683
|
+
let severity = 0;
|
|
1684
|
+
if (sample.lossRatio >= t.criticalLossRatio) severity = Math.max(severity, 3);
|
|
1685
|
+
else if (sample.lossRatio >= t.poorLossRatio) severity = Math.max(severity, 2);
|
|
1686
|
+
else if (sample.lossRatio >= t.fairLossRatio) severity = Math.max(severity, 1);
|
|
1687
|
+
if (sample.rttMs !== null) {
|
|
1688
|
+
if (sample.rttMs >= t.criticalRttMs) severity = Math.max(severity, 3);
|
|
1689
|
+
else if (sample.rttMs >= t.poorRttMs) severity = Math.max(severity, 2);
|
|
1690
|
+
else if (sample.rttMs >= t.fairRttMs) severity = Math.max(severity, 1);
|
|
1691
|
+
}
|
|
1692
|
+
if (sample.jitterMs !== null) {
|
|
1693
|
+
if (sample.jitterMs >= t.poorJitterMs) severity = Math.max(severity, 2);
|
|
1694
|
+
else if (sample.jitterMs >= t.fairJitterMs) severity = Math.max(severity, 1);
|
|
1695
|
+
}
|
|
1696
|
+
if (sample.qualityLimitationReason === "bandwidth") severity = Math.max(severity, 1);
|
|
1697
|
+
return qualityLevels[severity];
|
|
1698
|
+
}
|
|
1699
|
+
// updateConnectionQuality commits level transitions with hysteresis: an
|
|
1700
|
+
// improvement is reported on the first better sample, while a degradation must
|
|
1701
|
+
// persist for connectionQualityDebounceSamples consecutive samples to commit,
|
|
1702
|
+
// so a single blip does not flap the reported level.
|
|
1703
|
+
updateConnectionQuality(sample) {
|
|
1704
|
+
const candidate = this.classifyQuality(sample);
|
|
1705
|
+
const current = this.currentQualityLevel;
|
|
1706
|
+
if (current === null || candidate === current) {
|
|
1707
|
+
this.qualityDowngradeStreak = 0;
|
|
1708
|
+
if (candidate !== current) {
|
|
1709
|
+
this.currentQualityLevel = candidate;
|
|
1710
|
+
this.opts.callbacks?.onConnectionQualityChange?.({ level: candidate, sample });
|
|
1711
|
+
}
|
|
1712
|
+
return;
|
|
1713
|
+
}
|
|
1714
|
+
if (qualitySeverity(candidate) < qualitySeverity(current)) {
|
|
1715
|
+
this.qualityDowngradeStreak = 0;
|
|
1716
|
+
this.currentQualityLevel = candidate;
|
|
1717
|
+
this.opts.callbacks?.onConnectionQualityChange?.({ level: candidate, sample });
|
|
1718
|
+
return;
|
|
1719
|
+
}
|
|
1720
|
+
const needed = Math.max(
|
|
1721
|
+
1,
|
|
1722
|
+
this.opts.connectionQualityDebounceSamples ?? defaultConnectionQualityDebounceSamples
|
|
1723
|
+
);
|
|
1724
|
+
this.qualityDowngradeStreak += 1;
|
|
1725
|
+
if (this.qualityDowngradeStreak >= needed) {
|
|
1726
|
+
this.qualityDowngradeStreak = 0;
|
|
1727
|
+
this.currentQualityLevel = candidate;
|
|
1728
|
+
this.opts.callbacks?.onConnectionQualityChange?.({ level: candidate, sample });
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1438
1731
|
async reportSelectedICEPath(pc) {
|
|
1439
1732
|
if (this.pc !== pc || this.stopped) return;
|
|
1440
1733
|
let stats;
|