@furious.luke/argus-js 0.4.0 → 0.5.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/README.md +127 -23
- package/dist/index.cjs +1442 -171
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +330 -23
- package/dist/index.d.ts +330 -23
- package/dist/index.js +1441 -171
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @furious.luke/argus-js
|
|
2
2
|
|
|
3
|
-
Browser client for
|
|
3
|
+
Browser client for [Argus](https://github.com/furious-luke/go-projects/tree/main/argus) WebRTC sessions. It establishes the reliable agent text channel and optionally publishes camera, screen, or microphone media, handling gateway selection, signaling, and ICE negotiation for you.
|
|
4
4
|
|
|
5
5
|
Zero runtime dependencies — it uses only the browser's built-in `WebSocket` and `RTCPeerConnection`.
|
|
6
6
|
|
|
@@ -9,9 +9,11 @@ Zero runtime dependencies — it uses only the browser's built-in `WebSocket` an
|
|
|
9
9
|
Argus is a distributed video ingestion service. A stream flows through three parties:
|
|
10
10
|
|
|
11
11
|
- **Your server** mints a short-lived **join token** using its secret API key (see the [Go client](https://github.com/furious-luke/go-projects/tree/main/argus/client), published as the `github.com/furious-luke/argus-go` module, or the control-plane HTTP API).
|
|
12
|
-
- **The browser** (this library) uses that token to
|
|
12
|
+
- **The browser** (this library) uses that token to establish a text-only or media-backed session with the nearest Argus media server. It never sees the API key.
|
|
13
13
|
- **The browser** relays the winning gateway URL and its region-scoped read token
|
|
14
|
-
back to your server
|
|
14
|
+
back to your server. The URL routes both frame reads and notifications to the
|
|
15
|
+
selected region; the read token authorizes frame reads only, while notifications
|
|
16
|
+
use the server-held control token.
|
|
15
17
|
|
|
16
18
|
This library is only the browser publishing half. It deliberately does **not** talk to the control plane or mint tokens — that requires your secret API key and must stay server-side.
|
|
17
19
|
|
|
@@ -21,7 +23,7 @@ This library is only the browser publishing half. It deliberately does **not** t
|
|
|
21
23
|
│ │ ◀─────────────────────── │ │ ◀── token + gateway_urls ──
|
|
22
24
|
│ argus-js │ 2. token + gateway_urls └────────────┘
|
|
23
25
|
│ │
|
|
24
|
-
│ │ 3.
|
|
26
|
+
│ │ 3. WebRTC text + optional media ────────▶ Argus media server
|
|
25
27
|
│ │ 4. frameReadToken + selectedGatewayURL ─▶ your server
|
|
26
28
|
└──────────┘
|
|
27
29
|
```
|
|
@@ -60,17 +62,34 @@ const publisher = new Publisher({
|
|
|
60
62
|
onConnectionStateChange: (state) => console.log("state:", state),
|
|
61
63
|
onRecoveryStateChange: (event) => console.log("media recovery:", event),
|
|
62
64
|
// Show a button or prompt. From its click handler, call captureScreen()
|
|
63
|
-
// and then publisher.
|
|
64
|
-
onRecoveryRequired: (event) => console.warn(
|
|
65
|
+
// and then publisher.publish(newStream, "screen").
|
|
66
|
+
onRecoveryRequired: (event) => console.warn(`${event.track} must be restarted`, event),
|
|
65
67
|
onError: (err) => console.error("publish error:", err),
|
|
66
68
|
},
|
|
67
69
|
});
|
|
68
70
|
|
|
69
71
|
// captureCamera() applies sensible capture defaults; any MediaStream works too.
|
|
72
|
+
// The second argument labels the track type (defaults to "camera").
|
|
70
73
|
const stream = await captureCamera();
|
|
71
|
-
await publisher.start(stream);
|
|
74
|
+
await publisher.start(stream, "camera");
|
|
72
75
|
```
|
|
73
76
|
|
|
77
|
+
For a typed agent that needs no initial media or capture permission, start the
|
|
78
|
+
same WebRTC session with only its ordered text data channel:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
await publisher.startTextOnly();
|
|
82
|
+
publisher.sendUserText(crypto.randomUUID(), "Hello");
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Camera, screen, microphone, and the optional inbound speech track can still be
|
|
86
|
+
added later through `publish`, `publishMicrophone`, and `enableSpeech`.
|
|
87
|
+
|
|
88
|
+
Each published track is labelled with a **track type** (`"camera"` or
|
|
89
|
+
`"screen"`). The label is declared to Argus during signaling, so frame reads and
|
|
90
|
+
change-notification subscriptions can address a specific track by type. A stream
|
|
91
|
+
may carry one track of each type at a time.
|
|
92
|
+
|
|
74
93
|
### Capture helpers
|
|
75
94
|
|
|
76
95
|
`captureCamera()` and `captureScreen()` wrap `getUserMedia` / `getDisplayMedia` with defaults tuned for streaming — a capped resolution and modest frame rate, audio off. They matter most for screen sharing: on HiDPI/Retina displays a raw `getDisplayMedia` captures at native resolution (often 3456px+ / effectively 4k), wasting upload bandwidth and downstream decode for no benefit. `captureScreen()` caps the width instead.
|
|
@@ -79,18 +98,34 @@ await publisher.start(stream);
|
|
|
79
98
|
import { captureScreen } from "@furious.luke/argus-js";
|
|
80
99
|
|
|
81
100
|
const stream = await captureScreen();
|
|
82
|
-
await publisher.start(stream);
|
|
101
|
+
await publisher.start(stream, "screen");
|
|
83
102
|
```
|
|
84
103
|
|
|
85
104
|
Both accept overrides (shallow-merged over the defaults), and any `MediaStream` you build yourself still works if you'd rather manage constraints directly.
|
|
86
105
|
|
|
87
|
-
###
|
|
106
|
+
### Adding and removing tracks live
|
|
88
107
|
|
|
89
|
-
|
|
108
|
+
Start with one track, then add or remove others without reconnecting. `publish`
|
|
109
|
+
adds a track of a given type (renegotiating in place); `unpublish` removes it.
|
|
110
|
+
Each supplied stream must contain exactly one video track. Publishing a second
|
|
111
|
+
track of a type replaces the first.
|
|
90
112
|
|
|
91
113
|
```ts
|
|
114
|
+
// Live camera already publishing from start(stream, "camera")...
|
|
92
115
|
const screen = await captureScreen();
|
|
93
|
-
await publisher.
|
|
116
|
+
await publisher.publish(screen, "screen"); // now streaming camera + screen
|
|
117
|
+
|
|
118
|
+
// Later, stop sharing the screen but keep the camera live.
|
|
119
|
+
await publisher.unpublish("screen");
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`replaceStream(stream, type?)` remains available as a convenience — it delegates
|
|
123
|
+
to `publish` and defaults to the `"camera"` type. It is handy from
|
|
124
|
+
`onRecoveryRequired` to swap in a freshly reacquired screen share:
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
const screen = await captureScreen();
|
|
128
|
+
await publisher.replaceStream(screen, "screen");
|
|
94
129
|
```
|
|
95
130
|
|
|
96
131
|
### Stopping
|
|
@@ -101,13 +136,50 @@ publisher.stop(); // stops all tracks and tears down the peer connection
|
|
|
101
136
|
|
|
102
137
|
## API
|
|
103
138
|
|
|
139
|
+
### Assistant speech and text
|
|
140
|
+
|
|
141
|
+
Every publisher creates an ordered, reliable `argus.text` data channel. Enable
|
|
142
|
+
the persistent outbound speech track only after the user opts in:
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
const publisher = new Publisher({
|
|
146
|
+
gatewayURLs: join.gateway_urls,
|
|
147
|
+
token: join.token,
|
|
148
|
+
callbacks: {
|
|
149
|
+
onSpeechTrack(track) {
|
|
150
|
+
const audio = new Audio();
|
|
151
|
+
audio.srcObject = new MediaStream([track]);
|
|
152
|
+
void audio.play();
|
|
153
|
+
},
|
|
154
|
+
onAssistantText({ utteranceId, text }) {
|
|
155
|
+
renderCaption(utteranceId, text);
|
|
156
|
+
},
|
|
157
|
+
onUserTextResult(result) {
|
|
158
|
+
console.log(result.messageId, result.accepted);
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
await publisher.startTextOnly();
|
|
164
|
+
await publisher.enableSpeech(); // call from the application's user action
|
|
165
|
+
publisher.sendUserText(crypto.randomUUID(), "Stop and explain that again");
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
`enableSpeech()` is idempotent. The remote track is labelled `speech` and stays
|
|
169
|
+
attached but silent between utterances. `sendUserText` is admitted only while
|
|
170
|
+
the customer server owns a live control-token notify subscription.
|
|
171
|
+
|
|
104
172
|
### `new Publisher(options)`
|
|
105
173
|
|
|
106
174
|
| Option | Type | Description |
|
|
107
175
|
| --- | --- | --- |
|
|
108
|
-
| `gatewayURLs` | `string[]` | **Required.** The `gateway_urls` from the join response. All are raced simultaneously; the
|
|
176
|
+
| `gatewayURLs` | `string[]` | **Required.** The `gateway_urls` from the join response. All are raced simultaneously; the first to return `ready` wins. |
|
|
109
177
|
| `token` | `string` | **Required.** The short-lived join token from the join response. |
|
|
110
178
|
| `iceServers` | `RTCIceServer[]` | Optional extra ICE servers (e.g. your own STUN). TURN is supplied automatically by the winning gateway. |
|
|
179
|
+
| `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). |
|
|
180
|
+
| `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. |
|
|
181
|
+
| `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. |
|
|
182
|
+
| `peerConnectionTimeoutMs` | `number` | Deadline after the initial offer for WebRTC to reach `connected`. Defaults to 30 seconds. |
|
|
111
183
|
| `signalingReconnectTimeoutMs` | `number` | How long to retry a dropped signaling socket against the selected regional gateway. Defaults to 20 seconds. |
|
|
112
184
|
| `callbacks` | `PublisherCallbacks` | Optional lifecycle callbacks (see below). |
|
|
113
185
|
|
|
@@ -115,11 +187,19 @@ publisher.stop(); // stops all tracks and tears down the peer connection
|
|
|
115
187
|
|
|
116
188
|
| Member | Description |
|
|
117
189
|
| --- | --- |
|
|
118
|
-
| `start(stream)` | Races the gateways, completes the handshake, and sends the SDP offer. Resolves once the offer is sent — use `onConnected` to know when media is actually flowing. |
|
|
119
|
-
| `
|
|
190
|
+
| `start(stream, type?)` | Races the gateways, completes the handshake, and sends the SDP offer. The stream must contain exactly one video track; `type` labels it, defaulting to `"camera"`. Resolves once the offer is sent — use `onConnected` to know when media is actually flowing. |
|
|
191
|
+
| `startTextOnly()` | Starts with only the reliable ordered `argus.text` data channel and requests no capture permission. Media or speech may be added later. |
|
|
192
|
+
| `startAudioOnly(stream)` | Starts with the stream's single microphone track plus the text data channel. |
|
|
193
|
+
| `publish(stream, type)` | Adds the stream's single video track under `type` and renegotiates in place. Replaces any existing track of the same type. |
|
|
194
|
+
| `publishMicrophone(stream)` | Adds or replaces the session's microphone track and renegotiates in place (turns on transcription). |
|
|
195
|
+
| `unpublish(type)` | Removes the live track(s) of `type`, stops their capture, and renegotiates so Argus ends that track. |
|
|
196
|
+
| `unpublishMicrophone()` | Removes the microphone track and renegotiates (ends transcription). |
|
|
197
|
+
| `enableSpeech()` | Opts into the inbound `speech` track for text-to-speech. Idempotent; call from a user action. |
|
|
198
|
+
| `sendUserText(messageId, text)` | Sends typed input over the `argus.text` data channel (≤4 KiB). |
|
|
199
|
+
| `replaceStream(stream, type?)` | Convenience wrapper over `publish`; defaults to `"camera"`. |
|
|
120
200
|
| `stop()` | Stops all local tracks and closes the peer connection. |
|
|
121
201
|
| `frameReadToken` | The one-hour read token from the gateway's `ready` message, or `null` before connecting. The publisher uses it internally to resume signaling; hand it to your server to fetch frames. |
|
|
122
|
-
| `selectedGatewayURL` | The signaling URL that won the regional race, or `null` before connecting. Relay it
|
|
202
|
+
| `selectedGatewayURL` | The signaling URL that won the regional race, or `null` before connecting. Relay it so your server uses the same region for frame reads and notifications; `frameReadToken` authorizes only the frame reads. |
|
|
123
203
|
| `peerConnection` | The underlying `RTCPeerConnection`, or `null` if not started. |
|
|
124
204
|
| `isConnected` | `true` when the peer connection state is `"connected"`. |
|
|
125
205
|
|
|
@@ -131,13 +211,20 @@ publisher.stop(); // stops all tracks and tears down the peer connection
|
|
|
131
211
|
| `onConnectionStateChange(state)` | The `RTCPeerConnectionState` changed. |
|
|
132
212
|
| `onRecoveryStateChange(event)` | Argus detected stalled media and the publisher started, escalated, completed, or failed automatic recovery. |
|
|
133
213
|
| `onRecoveryRequired(event)` | Automatic recovery could not restore media, or capture ended and the host must ask the user for a new screen share. |
|
|
134
|
-
| `
|
|
214
|
+
| `onSpeechTrack(track, streams)` | The inbound `speech` track arrived after `enableSpeech()` — attach it to an `<audio>` element to play text-to-speech. |
|
|
215
|
+
| `onAssistantText({ utteranceId, text })` | Assistant text arrived; it is paced with synthesized speech when speech is enabled and delivered immediately in text-only mode. |
|
|
216
|
+
| `onUserTextResult({ messageId, accepted, reason })` | The server accepted or rejected a `sendUserText` message. |
|
|
217
|
+
| `onError(error)` | A fatal error occurred (signaling error, WebRTC connection failure/timeout, or signaling resume timed out). |
|
|
135
218
|
|
|
136
219
|
## How `start()` works
|
|
137
220
|
|
|
138
221
|
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.
|
|
139
222
|
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`.
|
|
140
|
-
3. **WebRTC.** A peer connection
|
|
223
|
+
3. **WebRTC.** A peer connection and the text data channel are created. Any
|
|
224
|
+
initial media track is added and labelled, then an offer is sent. Remote ICE
|
|
225
|
+
candidates that arrive before the SDP answer are buffered and flushed once
|
|
226
|
+
the answer is applied. Locally gathered candidates remain queued until their
|
|
227
|
+
signaling write succeeds and are replayed after signaling resume.
|
|
141
228
|
|
|
142
229
|
After this initial race, the publisher is pinned to the selected regional
|
|
143
230
|
gateway. If signaling drops, it reconnects to that same gateway with the
|
|
@@ -145,6 +232,22 @@ one-hour read token and waits for `resumed`; it does not race regions, rebuild
|
|
|
145
232
|
the peer connection, or repeat the `ready` handshake. If the retry deadline
|
|
146
233
|
expires, the publisher closes the stream and calls `onError`.
|
|
147
234
|
|
|
235
|
+
Leave both transport policies at their defaults for normal use. To prove the
|
|
236
|
+
firewall-friendly TURN TLS path from a VPN or restrictive network, force both
|
|
237
|
+
relay ICE and TLS TURN:
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
const publisher = new Publisher({
|
|
241
|
+
gatewayURLs: join.gateway_urls,
|
|
242
|
+
token: join.token,
|
|
243
|
+
iceTransportPolicy: "relay",
|
|
244
|
+
turnTransportPolicy: "tls",
|
|
245
|
+
});
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
This diagnostic configuration fails startup when the winning gateway did not
|
|
249
|
+
advertise a TLS TURN URL instead of silently testing another path.
|
|
250
|
+
|
|
148
251
|
## Automatic media recovery
|
|
149
252
|
|
|
150
253
|
Argus measures freshness from complete encoded samples received by the media
|
|
@@ -155,11 +258,12 @@ first detaches and reattaches the live sender and renegotiates. If samples do no
|
|
|
155
258
|
resume within four seconds, it performs an ICE restart and waits another eight
|
|
156
259
|
seconds. These fixed timings deliberately are not application configuration.
|
|
157
260
|
|
|
158
|
-
`onRecoveryStateChange` reports the recovery transitions
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
261
|
+
`onRecoveryStateChange` reports the recovery transitions, and the event's `track`
|
|
262
|
+
field names the affected track type. If both automatic steps fail,
|
|
263
|
+
`onRecoveryRequired` is called and the host can acquire a replacement stream and
|
|
264
|
+
pass it to `publish` (or `replaceStream`) for that track type. A browser cannot
|
|
265
|
+
silently reacquire a screen share after the user or operating system ends it, so
|
|
266
|
+
`capture_ended` always requires host UI and a fresh `captureScreen()` call.
|
|
163
267
|
|
|
164
268
|
## Browser support
|
|
165
269
|
|
|
@@ -176,7 +280,7 @@ npm run typecheck # tsc --noEmit
|
|
|
176
280
|
|
|
177
281
|
## Publishing
|
|
178
282
|
|
|
179
|
-
The package is published to npm under the `@furious
|
|
283
|
+
The package is published to npm under the `@furious.luke` scope. `prepublishOnly` runs typecheck, tests, and build first, so a release is:
|
|
180
284
|
|
|
181
285
|
```bash
|
|
182
286
|
npm version <patch|minor|major>
|