@furious.luke/argus-js 0.5.4 → 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 CHANGED
@@ -66,6 +66,7 @@ const publisher = new Publisher({
66
66
  });
67
67
  },
68
68
  onConnectionStateChange: (state) => console.log("state:", state),
69
+ onConnectionQualityChange: ({ level }) => console.log("connection quality:", level),
69
70
  onRecoveryStateChange: (event) => console.log("media recovery:", event),
70
71
  // Show a button or prompt. From its click handler, call captureScreen()
71
72
  // and then publisher.publish(newStream, "screen").
@@ -158,7 +159,10 @@ const publisher = new Publisher({
158
159
  void audio.play();
159
160
  },
160
161
  onAssistantText({ utteranceId, text }) {
161
- renderCaption(utteranceId, text);
162
+ appendCaption(utteranceId, text); // APPEND to the bubble keyed by utteranceId
163
+ },
164
+ onAssistantTextFinished({ utteranceId }) {
165
+ finalizeCaption(utteranceId); // this utterance's caption is complete
162
166
  },
163
167
  onUserTextResult(result) {
164
168
  console.log(result.messageId, result.accepted);
@@ -175,19 +179,32 @@ publisher.sendUserText(crypto.randomUUID(), "Stop and explain that again");
175
179
  attached but silent between utterances. `sendUserText` is admitted only while
176
180
  the customer server owns a live control-token notify subscription.
177
181
 
182
+ One assistant reply arrives as **many** `onAssistantText` chunks paced to the
183
+ audio, all sharing the **same `utteranceId`**. Group by that id and **append**
184
+ each chunk to the one caption element — creating a new element per callback is
185
+ what scatters a single reply across the UI. Chunks travel a reliable, ordered
186
+ data channel, so appending in arrival order reconstructs the text (there is no
187
+ sequence number). Treat `onAssistantTextFinished` (same `utteranceId`, emitted on
188
+ that same ordered channel after the last chunk) as the completion boundary —
189
+ not the `utterance_finished` lifecycle event, which can overtake the last chunk.
190
+
178
191
  ### `new Publisher(options)`
179
192
 
180
193
  | Option | Type | Description |
181
194
  | --- | --- | --- |
182
- | `gatewayURLs` | `string[]` | **Required.** The `gateway_urls` from the join response. All are raced simultaneously; the first to return `ready` wins. |
195
+ | `gatewayURLs` | `string[]` | **Required.** The `gateway_urls` from the join response. All are opened at once; the region whose acknowledgement (`accepted`) returns first is selected on network path, and only that region places the stream. The rest are held as standbys to fail over to. |
183
196
  | `token` | `string` | **Required.** The short-lived join token from the join response. |
184
197
  | `iceServers` | `RTCIceServer[]` | Optional extra ICE servers (e.g. your own STUN). TURN is supplied automatically by the winning gateway. |
185
198
  | `iceTransportPolicy` | `RTCIceTransportPolicy` | Passed to the underlying `RTCPeerConnection`. Defaults to `"all"`. Set `"relay"` to force media through TURN only (verifies the relay path end to end). |
186
199
  | `turnTransportPolicy` | `"all" \| "udp" \| "tls"` | Restricts gateway TURN URLs. Defaults to `"all"`; use `"tls"` with relay-only ICE to verify TURN over TLS. Startup fails if the required transport was not advertised. |
187
- | `gatewayHandshakeTimeoutMs` | `number` | Overall deadline for the initial gateway race and `accepted` `ready` handshake. Unaccepted sockets are replaced after 3 seconds so a blackholed TCP flow cannot consume the full deadline. Defaults to 20 seconds. |
200
+ | `gatewayHandshakeTimeoutMs` | `number` | Overall deadline for the whole gateway race selection, placement, and any failovers. Unaccepted sockets are replaced after 3 seconds so a blackholed TCP flow cannot consume the full deadline. Defaults to 20 seconds. |
201
+ | `gatewayFailoverTimeoutMs` | `number` | How long the selected region has to return `ready` before the publisher fails over to the next-fastest standby. A socket that closes or errors fails over immediately regardless; this is the backstop for a region that goes silent. Kept generous so a slow-but-good region is not abandoned for one that is merely closer to the control plane. Defaults to 8 seconds, capped at 20. |
188
202
  | `peerConnectionTimeoutMs` | `number` | Deadline after the initial offer for WebRTC to reach `connected`. Defaults to 30 seconds. |
189
203
  | `signalingReconnectTimeoutMs` | `number` | How long to retry a dropped signaling socket against the selected regional gateway. Defaults to 20 seconds. |
190
204
  | `preferredVideoCodecs` | `string[]` | Preferred video codecs, most-preferred first, as RTP MIME types (e.g. `"video/VP9"`, `"video/H264"`). Each published video track offers these ahead of the rest, so the browser sends the first one the media server also accepts. Defaults to `["video/VP9"]`. Pass `[]` to leave the browser's native order untouched. Codecs the browser lacks (or `setCodecPreferences` support, e.g. older Safari) are ignored — negotiation always falls back cleanly. |
205
+ | `connectionStatsIntervalMs` | `number` | How often to poll `RTCPeerConnection.getStats()` for connection-quality assessment while connected. Defaults to 2000. Set to `0` to disable stats polling entirely (`onConnectionStats`/`onConnectionQualityChange` will not fire). |
206
+ | `connectionQualityThresholds` | `Partial<ConnectionQualityThresholds>` | Overrides for the quality classification thresholds. Packet loss is the primary axis; RTT and jitter can only push the level worse, never better. Any omitted field keeps its default (see below). |
207
+ | `connectionQualityDebounceSamples` | `number` | Consecutive worse-than-current samples required before a downgrade is committed, damping transient blips. Improvements are reported on the first better sample. Defaults to 2. |
191
208
  | `callbacks` | `PublisherCallbacks` | Optional lifecycle callbacks (see below). |
192
209
 
193
210
  ### Methods & properties
@@ -219,15 +236,19 @@ the customer server owns a live control-token notify subscription.
219
236
  | `onRecoveryStateChange(event)` | Argus detected stalled media and the publisher started, escalated, completed, or failed automatic recovery. |
220
237
  | `onRecoveryRequired(event)` | Automatic recovery could not restore media, or capture ended and the host must ask the user for a new screen share. |
221
238
  | `onSpeechTrack(track, streams)` | The inbound `speech` track arrived after `enableSpeech()` — attach it to an `<audio>` element to play text-to-speech. |
222
- | `onAssistantText({ utteranceId, text })` | Assistant text arrived; it is paced with synthesized speech when speech is enabled and delivered immediately in text-only mode. |
239
+ | `onAssistantText({ utteranceId, text })` | One caption chunk arrived; paced with synthesized speech when speech is enabled, immediate in text-only mode. An utterance emits many chunks sharing one `utteranceId` — append them to the bubble keyed by that id rather than rendering each separately. |
240
+ | `onAssistantTextFinished({ utteranceId })` | The utterance's caption stream is complete. Emitted on the same ordered channel after its last `onAssistantText` chunk; finalize the visible caption here rather than on `utterance_finished`. |
223
241
  | `onUserTextResult({ messageId, accepted, reason })` | The server accepted or rejected a `sendUserText` message. |
242
+ | `onConnectionStats(sample)` | A periodic `ConnectionStatsSample` (loss ratio, RTT, jitter, send/available bitrate, encoder limitation) — the raw feed behind `onConnectionQualityChange`. Fires every `connectionStatsIntervalMs` while connected. |
243
+ | `onConnectionQualityChange({ level, sample })` | The derived connection-quality level (`"good" \| "fair" \| "poor" \| "critical"`) changed, including the first assessment after connecting. Downgrades are debounced; upgrades fire immediately. React to a degrading uplink here — warn the user, or drop a secondary track. |
224
244
  | `onError(error)` | A fatal error occurred (signaling error, WebRTC connection failure/timeout, or signaling resume timed out). |
225
245
 
226
246
  ## How `start()` works
227
247
 
228
- 1. **Gateway race.** Every URL in `gatewayURLs` is opened at once with the token in the query string. The first to complete the two-phase handshake (`accepted` `proceed` `ready`) wins; the rest are closed. This picks the lowest-latency region without a separate probe.
229
- 2. **TURN + read token.** The winning gateway's `ready` message carries per-session TURN credentials (merged into the ICE configuration) and the read token exposed as `frameReadToken`.
230
- 3. **WebRTC.** A peer connection and the text data channel are created. Any
248
+ 1. **Gateway race.** Every URL in `gatewayURLs` is opened at once with the token in the query string. Selection and placement are two separate steps: the region whose `accepted` returns *first* is selected — purely on network path, without waiting on any placement work — and the browser sends `proceed` to that one only, holding the rest open as standbys. This picks the lowest-latency region without a separate probe, and keeps a region's distance to the control plane out of the choice.
249
+ 2. **Placement, with failover.** The selected region does its placement work and returns `ready`. If its socket closes or errors, the browser fails over to the next-fastest standby immediately; if it just goes silent, it fails over after `gatewayFailoverTimeoutMs`. A region that answers `unavailable` (transiently unable to serve) is retried after a backoff rather than counted out. A region that reports the stream is already bound elsewhere sends a `placement_redirect`, and the browser reconnects to the region that holds it — so a mistimed failover self-heals.
250
+ 3. **TURN + read token.** The selected gateway's `ready` message carries per-session TURN credentials (merged into the ICE configuration) and the read token exposed as `frameReadToken`.
251
+ 4. **WebRTC.** A peer connection and the text data channel are created. Any
231
252
  initial media track is added and labelled, then an offer is sent. Remote ICE
232
253
  candidates that arrive before the SDP answer are buffered and flushed once
233
254
  the answer is applied. Locally gathered candidates remain queued until their
@@ -272,6 +293,53 @@ pass it to `publish` (or `replaceStream`) for that track type. A browser cannot
272
293
  silently reacquire a screen share after the user or operating system ends it, so
273
294
  `capture_ended` always requires host UI and a fresh `captureScreen()` call.
274
295
 
296
+ ## Connection quality
297
+
298
+ While connected, the publisher polls the peer connection's WebRTC stats every
299
+ `connectionStatsIntervalMs` (default 2s) and derives a coarse quality level so
300
+ you can react to a degrading uplink without parsing `getStats()` yourself. Each
301
+ poll delivers a raw `ConnectionStatsSample` to `onConnectionStats`; whenever the
302
+ derived level changes it delivers a `ConnectionQuality` to
303
+ `onConnectionQualityChange`.
304
+
305
+ ```ts
306
+ const pub = new Publisher({
307
+ gatewayURLs,
308
+ token,
309
+ callbacks: {
310
+ onConnectionQualityChange: ({ level, sample }) => {
311
+ if (level === "poor" || level === "critical") {
312
+ showBanner(`Weak connection — ${Math.round(sample.lossRatio * 100)}% packet loss`);
313
+ } else {
314
+ hideBanner();
315
+ }
316
+ },
317
+ },
318
+ });
319
+ ```
320
+
321
+ Packet loss is the primary signal; round-trip time, jitter, and a
322
+ bandwidth-limited encoder can only push the level worse, never better. To avoid
323
+ flapping on a momentary blip, **downgrades** are debounced — they require
324
+ `connectionQualityDebounceSamples` (default 2) consecutive worse samples before
325
+ committing — while **upgrades** are reported on the first improved sample so
326
+ recovery is reflected promptly. The first sample after connecting always emits
327
+ the baseline level.
328
+
329
+ The default `ConnectionQualityThresholds` (all overridable via
330
+ `connectionQualityThresholds`):
331
+
332
+ | Level | Packet loss | RTT | Jitter |
333
+ | --- | --- | --- | --- |
334
+ | `fair` | ≥ 2% | ≥ 300 ms | ≥ 50 ms |
335
+ | `poor` | ≥ 5% | ≥ 600 ms | ≥ 150 ms |
336
+ | `critical` | ≥ 12% | ≥ 1000 ms | — |
337
+
338
+ Loss ratio and send bitrate are windowed over each interval; RTT, jitter, and
339
+ available bitrate are point-in-time. A sample field is `null` when the browser
340
+ did not report the underlying stat. This is a detection surface only — the
341
+ publisher does not itself lower bitrate or resolution in response.
342
+
275
343
  ## Browser support
276
344
 
277
345
  Requires a browser with WebRTC (`RTCPeerConnection`) and `WebSocket` — all current evergreen browsers. There is no Node.js runtime support; this is a browser-only package.