@furious.luke/argus-js 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Luke Hodkinson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,145 @@
1
+ # @furious.luke/argus-js
2
+
3
+ Browser client for the [Argus](https://github.com/furious-luke/go-projects/tree/main/argus) video streaming platform. It publishes a `MediaStream` (camera, screen share, etc.) from a browser to an Argus media server over WebRTC, handling gateway selection, the signaling handshake, and ICE negotiation for you.
4
+
5
+ Zero runtime dependencies — it uses only the browser's built-in `WebSocket` and `RTCPeerConnection`.
6
+
7
+ ## Where this fits
8
+
9
+ Argus is a distributed video ingestion service. A stream flows through three parties:
10
+
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 publish video to the nearest Argus media server. It never sees the API key.
13
+ - **Your server** later fetches frames from the stream on demand.
14
+
15
+ 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.
16
+
17
+ ```
18
+ ┌──────────┐ 1. request join token ┌────────────┐
19
+ │ browser │ ───────────────────────▶ │ your server│ ──(API key)──▶ Argus control plane
20
+ │ │ ◀─────────────────────── │ │ ◀── token + gateway_urls ──
21
+ │ argus-js │ 2. token + gateway_urls └────────────┘
22
+ │ │
23
+ │ │ 3. publish WebRTC video ────────────────▶ Argus media server
24
+ └──────────┘
25
+ ```
26
+
27
+ ## Installation
28
+
29
+ ```bash
30
+ npm install @furious.luke/argus-js
31
+ ```
32
+
33
+ ## Usage
34
+
35
+ Your server first creates a stream and returns the join token bundle to the browser (the `token` and `gateway_urls` from Argus's `POST /api/streams` response). Then:
36
+
37
+ ```ts
38
+ import { Publisher, captureCamera } from "@furious.luke/argus-js";
39
+
40
+ // `join` is the { token, gateway_urls } bundle your server obtained from Argus.
41
+ const publisher = new Publisher({
42
+ gatewayURLs: join.gateway_urls,
43
+ token: join.token,
44
+ callbacks: {
45
+ onConnected: () => console.log("streaming live"),
46
+ onConnectionStateChange: (state) => console.log("state:", state),
47
+ onError: (err) => console.error("publish error:", err),
48
+ },
49
+ });
50
+
51
+ // captureCamera() applies sensible capture defaults; any MediaStream works too.
52
+ const stream = await captureCamera();
53
+ await publisher.start(stream);
54
+
55
+ // After start() resolves, this token lets your server fetch frames for the stream.
56
+ console.log("read token:", publisher.frameReadToken);
57
+ ```
58
+
59
+ ### Capture helpers
60
+
61
+ `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.
62
+
63
+ ```ts
64
+ import { captureScreen } from "@furious.luke/argus-js";
65
+
66
+ const stream = await captureScreen();
67
+ await publisher.start(stream);
68
+ ```
69
+
70
+ Both accept overrides (shallow-merged over the defaults), and any `MediaStream` you build yourself still works if you'd rather manage constraints directly.
71
+
72
+ ### Switching sources without reconnecting
73
+
74
+ `replaceStream` renegotiates in place, e.g. to toggle between camera and screen:
75
+
76
+ ```ts
77
+ const screen = await captureScreen();
78
+ await publisher.replaceStream(screen);
79
+ ```
80
+
81
+ ### Stopping
82
+
83
+ ```ts
84
+ publisher.stop(); // stops all tracks and tears down the peer connection
85
+ ```
86
+
87
+ ## API
88
+
89
+ ### `new Publisher(options)`
90
+
91
+ | Option | Type | Description |
92
+ | --- | --- | --- |
93
+ | `gatewayURLs` | `string[]` | **Required.** The `gateway_urls` from the join response. All are raced simultaneously; the fastest to accept wins. |
94
+ | `token` | `string` | **Required.** The short-lived join token from the join response. |
95
+ | `iceServers` | `RTCIceServer[]` | Optional extra ICE servers (e.g. your own STUN). TURN is supplied automatically by the winning gateway. |
96
+ | `callbacks` | `PublisherCallbacks` | Optional lifecycle callbacks (see below). |
97
+
98
+ ### Methods & properties
99
+
100
+ | Member | Description |
101
+ | --- | --- |
102
+ | `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. |
103
+ | `replaceStream(stream)` | Replaces the published tracks and renegotiates in place. |
104
+ | `stop()` | Stops all local tracks and closes the peer connection. |
105
+ | `frameReadToken` | The read token from the gateway's `ready` message, or `null` before connecting. Hand this to your server to fetch frames. |
106
+ | `peerConnection` | The underlying `RTCPeerConnection`, or `null` if not started. |
107
+ | `isConnected` | `true` when the peer connection state is `"connected"`. |
108
+
109
+ ### `PublisherCallbacks`
110
+
111
+ | Callback | When |
112
+ | --- | --- |
113
+ | `onConnected()` | The peer connection reached `"connected"` — media is flowing. |
114
+ | `onConnectionStateChange(state)` | The `RTCPeerConnectionState` changed. |
115
+ | `onError(error)` | A fatal error occurred (signaling error, gateway failure, or closed socket). |
116
+
117
+ ## How `start()` works
118
+
119
+ 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.
120
+ 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`.
121
+ 3. **WebRTC.** A peer connection is created, tracks are added, and an offer is sent. Remote ICE candidates that arrive before the SDP answer are buffered and flushed once the answer is applied.
122
+
123
+ ## Browser support
124
+
125
+ 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.
126
+
127
+ ## Development
128
+
129
+ ```bash
130
+ npm install
131
+ npm run build # bundle ESM + CJS + types into dist/ via tsup
132
+ npm test # run the vitest suite (jsdom)
133
+ npm run typecheck # tsc --noEmit
134
+ ```
135
+
136
+ ## Publishing
137
+
138
+ The package is published to npm under the `@furious-luke` scope. `prepublishOnly` runs typecheck, tests, and build first, so a release is:
139
+
140
+ ```bash
141
+ npm version <patch|minor|major>
142
+ npm publish
143
+ ```
144
+
145
+ (The scope is configured for public access via `publishConfig`.)
package/dist/index.cjs ADDED
@@ -0,0 +1,337 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
+ Publisher: () => Publisher,
24
+ captureCamera: () => captureCamera,
25
+ captureScreen: () => captureScreen
26
+ });
27
+ module.exports = __toCommonJS(src_exports);
28
+
29
+ // src/signaling.ts
30
+ var SignalingChannel = class _SignalingChannel {
31
+ ws;
32
+ /** Fired for every incoming JSON message. */
33
+ onMessage = null;
34
+ /** Fired when the underlying WebSocket closes. */
35
+ onClose = null;
36
+ /** Fired when an error occurs on the WebSocket. */
37
+ onError = null;
38
+ constructor(ws) {
39
+ this.ws = ws;
40
+ }
41
+ /**
42
+ * Adopts an already-open WebSocket (e.g. the winner of a gateway race),
43
+ * routing its events through the channel's callbacks. Any handlers previously
44
+ * attached to the socket are replaced.
45
+ */
46
+ static wrap(ws) {
47
+ const ch = new _SignalingChannel(ws);
48
+ ws.onmessage = (ev) => {
49
+ const msg = parseSignal(ev.data);
50
+ if (msg) ch.onMessage?.(msg);
51
+ };
52
+ ws.onerror = () => ch.onError?.(new Error("WebSocket error"));
53
+ ws.onclose = () => ch.onClose?.();
54
+ return ch;
55
+ }
56
+ /** Sends a JSON message if the socket is open; a no-op otherwise. */
57
+ send(msg) {
58
+ if (this.ws.readyState === WebSocket.OPEN) {
59
+ this.ws.send(JSON.stringify(msg));
60
+ }
61
+ }
62
+ /** Closes the underlying WebSocket. */
63
+ close() {
64
+ this.ws.close();
65
+ }
66
+ };
67
+ function parseSignal(data) {
68
+ try {
69
+ return JSON.parse(data);
70
+ } catch {
71
+ return null;
72
+ }
73
+ }
74
+
75
+ // src/publisher.ts
76
+ var Publisher = class {
77
+ opts;
78
+ sig = null;
79
+ pc = null;
80
+ hasAnswer = false;
81
+ pendingRemoteCandidates = [];
82
+ localStream = null;
83
+ readToken = null;
84
+ constructor(opts) {
85
+ this.opts = opts;
86
+ }
87
+ /** The read token received from the gateway after connecting, for fetching frames. */
88
+ get frameReadToken() {
89
+ return this.readToken;
90
+ }
91
+ /**
92
+ * Starts the publisher: races all gateways to find the fastest, completes
93
+ * the two-phase handshake, creates the peer connection, and sends the SDP
94
+ * offer. Resolves when the offer has been sent (not when ICE completes —
95
+ * use onConnected for that).
96
+ */
97
+ async start(stream) {
98
+ this.localStream = stream;
99
+ const { ws, readyInfo } = await this.raceGateways();
100
+ if (readyInfo.read_token) {
101
+ this.readToken = readyInfo.read_token;
102
+ }
103
+ const iceServers = [...this.opts.iceServers ?? []];
104
+ if (readyInfo.turn_url) {
105
+ iceServers.push({
106
+ urls: readyInfo.turn_url,
107
+ username: readyInfo.turn_username,
108
+ credential: readyInfo.turn_credential
109
+ });
110
+ }
111
+ this.pc = new RTCPeerConnection({ iceServers });
112
+ this.pc.onicecandidate = (ev) => {
113
+ if (!ev.candidate || !this.sig) return;
114
+ const c = ev.candidate;
115
+ this.sig.send({
116
+ type: "ice_candidate",
117
+ candidate: c.candidate,
118
+ sdp_mid: c.sdpMid ?? void 0,
119
+ sdp_mline_index: c.sdpMLineIndex ?? void 0,
120
+ username_fragment: c.usernameFragment ?? void 0
121
+ });
122
+ };
123
+ this.pc.onconnectionstatechange = () => {
124
+ const state = this.pc?.connectionState;
125
+ if (state) this.opts.callbacks?.onConnectionStateChange?.(state);
126
+ if (state === "connected") this.opts.callbacks?.onConnected?.();
127
+ };
128
+ for (const track of stream.getTracks()) {
129
+ this.pc.addTrack(track, stream);
130
+ }
131
+ const offer = await this.pc.createOffer();
132
+ await this.pc.setLocalDescription(offer);
133
+ await this.gatherComplete();
134
+ this.sig = SignalingChannel.wrap(ws);
135
+ this.sig.onMessage = (msg) => this.handleSignal(msg);
136
+ this.sig.onClose = () => this.opts.callbacks?.onError?.(new Error("signaling closed"));
137
+ this.sig.onError = (err) => this.opts.callbacks?.onError?.(err);
138
+ const local = this.pc.localDescription;
139
+ if (!local) throw new Error("local description missing after gather");
140
+ this.sig.send({ type: "offer", sdp: local.sdp, sdp_type: "offer" });
141
+ }
142
+ /** Replaces the currently published stream with a new one. */
143
+ async replaceStream(stream) {
144
+ if (!this.pc) throw new Error("publisher not started");
145
+ const senders = this.pc.getSenders();
146
+ for (const sender of senders) {
147
+ if (sender.track) {
148
+ this.pc.removeTrack(sender);
149
+ }
150
+ }
151
+ for (const track of stream.getTracks()) {
152
+ this.pc.addTrack(track, stream);
153
+ }
154
+ this.localStream = stream;
155
+ const offer = await this.pc.createOffer();
156
+ await this.pc.setLocalDescription(offer);
157
+ await this.gatherComplete();
158
+ const local = this.pc.localDescription;
159
+ if (!local) throw new Error("local description missing");
160
+ this.sig?.send({
161
+ type: "offer",
162
+ sdp: local.sdp,
163
+ sdp_type: "offer"
164
+ });
165
+ }
166
+ /** Stops publishing and tears down the peer connection. */
167
+ stop() {
168
+ this.sig?.close();
169
+ this.sig = null;
170
+ this.localStream?.getTracks().forEach((t) => t.stop());
171
+ this.localStream = null;
172
+ this.pc?.close();
173
+ this.pc = null;
174
+ this.hasAnswer = false;
175
+ this.pendingRemoteCandidates = [];
176
+ this.readToken = null;
177
+ }
178
+ /** Returns the current RTCPeerConnection, or null if not started. */
179
+ get peerConnection() {
180
+ return this.pc;
181
+ }
182
+ /** Returns true if the peer connection is in the "connected" state. */
183
+ get isConnected() {
184
+ return this.pc?.connectionState === "connected";
185
+ }
186
+ // -------------------------------------------------------------------------
187
+ // Private helpers
188
+ // -------------------------------------------------------------------------
189
+ raceGateways() {
190
+ return new Promise((resolve, reject) => {
191
+ const { gatewayURLs, token } = this.opts;
192
+ if (gatewayURLs.length === 0) {
193
+ reject(new Error("no gateway URLs provided"));
194
+ return;
195
+ }
196
+ const sockets = [];
197
+ let settled = false;
198
+ const closeAll = (except) => {
199
+ for (const s of sockets) {
200
+ if (s !== except) {
201
+ s.onmessage = null;
202
+ s.onerror = null;
203
+ s.onclose = null;
204
+ s.close();
205
+ }
206
+ }
207
+ };
208
+ const checkAllFailed = () => {
209
+ if (settled) return;
210
+ if (sockets.every((s) => s.readyState === WebSocket.CLOSED || s.readyState === WebSocket.CLOSING)) {
211
+ settled = true;
212
+ reject(new Error("all gateways failed to connect"));
213
+ }
214
+ };
215
+ for (const gatewayURL of gatewayURLs) {
216
+ const u = new URL(gatewayURL);
217
+ u.searchParams.set("token", token);
218
+ const ws = new WebSocket(u.toString());
219
+ sockets.push(ws);
220
+ let accepted = false;
221
+ ws.onmessage = (ev) => {
222
+ if (settled) return;
223
+ try {
224
+ const msg = JSON.parse(ev.data);
225
+ if (!accepted && msg.type === "accepted") {
226
+ accepted = true;
227
+ ws.send(JSON.stringify({ type: "proceed" }));
228
+ } else if (accepted && msg.type === "ready") {
229
+ settled = true;
230
+ closeAll(ws);
231
+ resolve({ ws, readyInfo: msg });
232
+ }
233
+ } catch {
234
+ }
235
+ };
236
+ ws.onerror = () => checkAllFailed();
237
+ ws.onclose = () => checkAllFailed();
238
+ }
239
+ });
240
+ }
241
+ handleSignal(msg) {
242
+ switch (msg.type) {
243
+ case "answer": {
244
+ if (!this.pc) return;
245
+ this.pc.setRemoteDescription(
246
+ new RTCSessionDescription({ type: "answer", sdp: msg.sdp })
247
+ ).then(() => {
248
+ this.hasAnswer = true;
249
+ for (const init of this.pendingRemoteCandidates) {
250
+ this.pc?.addIceCandidate(init).catch(() => {
251
+ });
252
+ }
253
+ this.pendingRemoteCandidates = [];
254
+ }).catch((err) => {
255
+ this.opts.callbacks?.onError?.(
256
+ new Error(`failed to set remote description: ${err}`)
257
+ );
258
+ });
259
+ break;
260
+ }
261
+ case "ice_candidate": {
262
+ if (!this.pc) return;
263
+ const init = {
264
+ candidate: msg.candidate,
265
+ sdpMid: msg.sdp_mid ?? null,
266
+ sdpMLineIndex: msg.sdp_mline_index ?? null,
267
+ usernameFragment: msg.username_fragment ?? null
268
+ };
269
+ if (this.hasAnswer) {
270
+ this.pc.addIceCandidate(init).catch(() => {
271
+ });
272
+ } else {
273
+ this.pendingRemoteCandidates.push(init);
274
+ }
275
+ break;
276
+ }
277
+ case "connection_state": {
278
+ break;
279
+ }
280
+ case "error": {
281
+ this.opts.callbacks?.onError?.(new Error(msg.error));
282
+ break;
283
+ }
284
+ }
285
+ }
286
+ /** Waits for ICE gathering to reach the "complete" state. */
287
+ gatherComplete() {
288
+ return new Promise((resolve) => {
289
+ const pc = this.pc;
290
+ if (!pc) {
291
+ resolve();
292
+ return;
293
+ }
294
+ if (pc.iceGatheringState === "complete") {
295
+ resolve();
296
+ return;
297
+ }
298
+ const handler = () => {
299
+ if (pc.iceGatheringState === "complete") {
300
+ pc.removeEventListener("icegatheringstatechange", handler);
301
+ resolve();
302
+ }
303
+ };
304
+ pc.addEventListener("icegatheringstatechange", handler);
305
+ });
306
+ }
307
+ };
308
+
309
+ // src/capture.ts
310
+ async function captureCamera(opts = {}) {
311
+ const constraints = {
312
+ video: opts.video ?? {
313
+ width: { ideal: 1280 },
314
+ height: { ideal: 720 },
315
+ frameRate: { ideal: 30 }
316
+ },
317
+ audio: opts.audio ?? false
318
+ };
319
+ return navigator.mediaDevices.getUserMedia(constraints);
320
+ }
321
+ async function captureScreen(opts = {}) {
322
+ const constraints = {
323
+ video: opts.video ?? {
324
+ width: { max: 1920 },
325
+ frameRate: { ideal: 5, max: 10 }
326
+ },
327
+ audio: opts.audio ?? false
328
+ };
329
+ return navigator.mediaDevices.getDisplayMedia(constraints);
330
+ }
331
+ // Annotate the CommonJS export names for ESM import in node:
332
+ 0 && (module.exports = {
333
+ Publisher,
334
+ captureCamera,
335
+ captureScreen
336
+ });
337
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/signaling.ts","../src/publisher.ts","../src/capture.ts"],"sourcesContent":["export { Publisher } from \"./publisher\";\nexport type {\n SignalMessage,\n PublisherCallbacks,\n PublisherOptions,\n GatewayReadyInfo,\n} from \"./types\";\nexport { captureCamera, captureScreen } from \"./capture\";\nexport type {\n CaptureCameraOptions,\n CaptureScreenOptions,\n} from \"./capture\";\n","import type { SignalMessage } from \"./types\";\n\n/**\n * Wraps an open WebSocket to the Argus gateway and handles the JSON message\n * envelope: incoming text frames are parsed into {@link SignalMessage}s and\n * outgoing messages are serialised.\n *\n * This is an internal helper. The {@link Publisher} opens the socket itself\n * (racing all candidate gateways) and hands the winner here, so this class only\n * ever adopts an already-open socket rather than dialing one.\n *\n * @internal\n */\nexport class SignalingChannel {\n private ws: WebSocket;\n\n /** Fired for every incoming JSON message. */\n onMessage: ((msg: SignalMessage) => void) | null = null;\n /** Fired when the underlying WebSocket closes. */\n onClose: (() => void) | null = null;\n /** Fired when an error occurs on the WebSocket. */\n onError: ((err: Error) => void) | null = null;\n\n private constructor(ws: WebSocket) {\n this.ws = ws;\n }\n\n /**\n * Adopts an already-open WebSocket (e.g. the winner of a gateway race),\n * routing its events through the channel's callbacks. Any handlers previously\n * attached to the socket are replaced.\n */\n static wrap(ws: WebSocket): SignalingChannel {\n const ch = new SignalingChannel(ws);\n ws.onmessage = (ev: MessageEvent) => {\n const msg = parseSignal(ev.data);\n if (msg) ch.onMessage?.(msg);\n };\n ws.onerror = () => ch.onError?.(new Error(\"WebSocket error\"));\n ws.onclose = () => ch.onClose?.();\n return ch;\n }\n\n /** Sends a JSON message if the socket is open; a no-op otherwise. */\n send(msg: SignalMessage): void {\n if (this.ws.readyState === WebSocket.OPEN) {\n this.ws.send(JSON.stringify(msg));\n }\n }\n\n /** Closes the underlying WebSocket. */\n close(): void {\n this.ws.close();\n }\n}\n\n/** Parses a WebSocket text frame into a SignalMessage, or null if malformed. */\nfunction parseSignal(data: unknown): SignalMessage | null {\n try {\n return JSON.parse(data as string) as SignalMessage;\n } catch {\n return null;\n }\n}\n","import { SignalingChannel } from \"./signaling\";\nimport type { GatewayReadyInfo, PublisherOptions, SignalMessage } from \"./types\";\n\n/**\n * Publisher streams a browser {@link MediaStream} to an Argus media server over\n * WebRTC. Given the `gateway_urls` and `token` from a join-token response, it\n * races the candidate gateways to the fastest one, completes the two-phase\n * signaling handshake, and manages the peer connection — offer/answer exchange,\n * ICE candidate trickling, and track (re)negotiation.\n *\n * After {@link Publisher.start} resolves, {@link Publisher.frameReadToken} holds\n * the token your application server needs to fetch frames for this stream.\n *\n * @example Publish the default camera:\n * ```ts\n * const pub = new Publisher({\n * gatewayURLs: joinResp.gateway_urls,\n * token: joinResp.token,\n * callbacks: { onConnected: () => console.log(\"live!\") },\n * });\n *\n * const stream = await navigator.mediaDevices.getUserMedia({ video: true });\n * await pub.start(stream);\n * ```\n */\nexport class Publisher {\n private opts: PublisherOptions;\n private sig: SignalingChannel | null = null;\n private pc: RTCPeerConnection | null = null;\n private hasAnswer = false;\n private pendingRemoteCandidates: RTCIceCandidateInit[] = [];\n private localStream: MediaStream | null = null;\n private readToken: string | null = null;\n\n constructor(opts: PublisherOptions) {\n this.opts = opts;\n }\n\n /** The read token received from the gateway after connecting, for fetching frames. */\n get frameReadToken(): string | null { return this.readToken; }\n\n /**\n * Starts the publisher: races all gateways to find the fastest, completes\n * the two-phase handshake, creates the peer connection, and sends the SDP\n * offer. Resolves when the offer has been sent (not when ICE completes —\n * use onConnected for that).\n */\n async start(stream: MediaStream): Promise<void> {\n this.localStream = stream;\n\n // Race all gateways; returns winning WebSocket + TURN/read-token info\n const { ws, readyInfo } = await this.raceGateways();\n\n // Store read token for caller\n if (readyInfo.read_token) {\n this.readToken = readyInfo.read_token;\n }\n\n // Build ICE servers: extra servers from opts + TURN from gateway\n const iceServers: RTCIceServer[] = [...(this.opts.iceServers ?? [])];\n if (readyInfo.turn_url) {\n iceServers.push({\n urls: readyInfo.turn_url,\n username: readyInfo.turn_username,\n credential: readyInfo.turn_credential,\n });\n }\n\n this.pc = new RTCPeerConnection({ iceServers });\n\n this.pc.onicecandidate = (ev) => {\n if (!ev.candidate || !this.sig) return;\n const c = ev.candidate;\n this.sig.send({\n type: \"ice_candidate\",\n candidate: c.candidate,\n sdp_mid: c.sdpMid ?? undefined,\n sdp_mline_index: c.sdpMLineIndex ?? undefined,\n username_fragment: c.usernameFragment ?? undefined,\n });\n };\n\n this.pc.onconnectionstatechange = () => {\n const state = this.pc?.connectionState;\n if (state) this.opts.callbacks?.onConnectionStateChange?.(state);\n if (state === \"connected\") this.opts.callbacks?.onConnected?.();\n };\n\n for (const track of stream.getTracks()) {\n this.pc.addTrack(track, stream);\n }\n\n const offer = await this.pc.createOffer();\n await this.pc.setLocalDescription(offer);\n await this.gatherComplete();\n\n // Wrap winning WebSocket in SignalingChannel\n this.sig = SignalingChannel.wrap(ws);\n this.sig.onMessage = (msg) => this.handleSignal(msg);\n this.sig.onClose = () => this.opts.callbacks?.onError?.(new Error(\"signaling closed\"));\n this.sig.onError = (err) => this.opts.callbacks?.onError?.(err);\n\n const local = this.pc.localDescription;\n if (!local) throw new Error(\"local description missing after gather\");\n this.sig.send({ type: \"offer\", sdp: local.sdp, sdp_type: \"offer\" });\n }\n\n /** Replaces the currently published stream with a new one. */\n async replaceStream(stream: MediaStream): Promise<void> {\n if (!this.pc) throw new Error(\"publisher not started\");\n\n // Remove old tracks.\n const senders = this.pc.getSenders();\n for (const sender of senders) {\n if (sender.track) {\n this.pc.removeTrack(sender);\n }\n }\n\n // Add new tracks.\n for (const track of stream.getTracks()) {\n this.pc.addTrack(track, stream);\n }\n this.localStream = stream;\n\n // Renegotiate.\n const offer = await this.pc.createOffer();\n await this.pc.setLocalDescription(offer);\n await this.gatherComplete();\n\n const local = this.pc.localDescription;\n if (!local) throw new Error(\"local description missing\");\n this.sig?.send({\n type: \"offer\",\n sdp: local.sdp,\n sdp_type: \"offer\",\n });\n }\n\n /** Stops publishing and tears down the peer connection. */\n stop(): void {\n this.sig?.close();\n this.sig = null;\n\n this.localStream?.getTracks().forEach((t) => t.stop());\n this.localStream = null;\n\n this.pc?.close();\n this.pc = null;\n this.hasAnswer = false;\n this.pendingRemoteCandidates = [];\n this.readToken = null;\n }\n\n /** Returns the current RTCPeerConnection, or null if not started. */\n get peerConnection(): RTCPeerConnection | null {\n return this.pc;\n }\n\n /** Returns true if the peer connection is in the \"connected\" state. */\n get isConnected(): boolean {\n return this.pc?.connectionState === \"connected\";\n }\n\n // -------------------------------------------------------------------------\n // Private helpers\n // -------------------------------------------------------------------------\n\n private raceGateways(): Promise<{ ws: WebSocket; readyInfo: GatewayReadyInfo }> {\n return new Promise((resolve, reject) => {\n const { gatewayURLs, token } = this.opts;\n if (gatewayURLs.length === 0) {\n reject(new Error(\"no gateway URLs provided\"));\n return;\n }\n\n const sockets: WebSocket[] = [];\n let settled = false;\n\n const closeAll = (except?: WebSocket) => {\n for (const s of sockets) {\n if (s !== except) {\n s.onmessage = null;\n s.onerror = null;\n s.onclose = null;\n s.close();\n }\n }\n };\n\n const checkAllFailed = () => {\n if (settled) return;\n if (sockets.every(s => s.readyState === WebSocket.CLOSED || s.readyState === WebSocket.CLOSING)) {\n settled = true;\n reject(new Error(\"all gateways failed to connect\"));\n }\n };\n\n for (const gatewayURL of gatewayURLs) {\n const u = new URL(gatewayURL);\n u.searchParams.set(\"token\", token);\n const ws = new WebSocket(u.toString());\n sockets.push(ws);\n\n let accepted = false;\n\n ws.onmessage = (ev: MessageEvent) => {\n if (settled) return;\n try {\n const msg = JSON.parse(ev.data as string);\n if (!accepted && msg.type === \"accepted\") {\n accepted = true;\n ws.send(JSON.stringify({ type: \"proceed\" }));\n } else if (accepted && msg.type === \"ready\") {\n settled = true;\n closeAll(ws);\n resolve({ ws, readyInfo: msg as GatewayReadyInfo });\n }\n } catch {\n /* ignore malformed */\n }\n };\n\n ws.onerror = () => checkAllFailed();\n ws.onclose = () => checkAllFailed();\n }\n });\n }\n\n private handleSignal(msg: SignalMessage): void {\n switch (msg.type) {\n case \"answer\": {\n if (!this.pc) return;\n this.pc\n .setRemoteDescription(\n new RTCSessionDescription({ type: \"answer\", sdp: msg.sdp }),\n )\n .then(() => {\n this.hasAnswer = true;\n // Flush any ICE candidates that arrived before the answer.\n for (const init of this.pendingRemoteCandidates) {\n this.pc?.addIceCandidate(init).catch(() => {\n /* ignore stale candidates */\n });\n }\n this.pendingRemoteCandidates = [];\n })\n .catch((err) => {\n this.opts.callbacks?.onError?.(\n new Error(`failed to set remote description: ${err}`),\n );\n });\n break;\n }\n\n case \"ice_candidate\": {\n if (!this.pc) return;\n const init: RTCIceCandidateInit = {\n candidate: msg.candidate,\n sdpMid: msg.sdp_mid ?? null,\n sdpMLineIndex: msg.sdp_mline_index ?? null,\n usernameFragment: msg.username_fragment ?? null,\n };\n if (this.hasAnswer) {\n this.pc.addIceCandidate(init).catch(() => {\n /* ignore stale candidates */\n });\n } else {\n this.pendingRemoteCandidates.push(init);\n }\n break;\n }\n\n case \"connection_state\": {\n // The server echoes the peer connection state; the onconnectionstatechange\n // handler above already covers this, but callers can also react via\n // onConnectionStateChange.\n break;\n }\n\n case \"error\": {\n this.opts.callbacks?.onError?.(new Error(msg.error));\n break;\n }\n }\n }\n\n /** Waits for ICE gathering to reach the \"complete\" state. */\n private gatherComplete(): Promise<void> {\n return new Promise((resolve) => {\n const pc = this.pc;\n if (!pc) {\n resolve();\n return;\n }\n if (pc.iceGatheringState === \"complete\") {\n resolve();\n return;\n }\n const handler = () => {\n if (pc.iceGatheringState === \"complete\") {\n pc.removeEventListener(\"icegatheringstatechange\", handler);\n resolve();\n }\n };\n pc.addEventListener(\"icegatheringstatechange\", handler);\n });\n }\n}\n","/**\n * Options for {@link captureCamera}.\n *\n * These are merged shallowly over the library defaults: any field you provide\n * replaces the default for that field entirely (e.g. passing `video` overrides\n * the default video constraints rather than merging into them). Omit a field to\n * keep its default.\n */\nexport interface CaptureCameraOptions {\n /**\n * Video constraints, or `true`/`false`. Defaults to a modest resolution and\n * frame rate (see {@link captureCamera}). Set `false` to disable video.\n */\n video?: MediaTrackConstraints | boolean;\n /**\n * Audio constraints, or `true`/`false`. Defaults to `false` — Argus is a\n * video-frame streaming system, so audio is off unless you ask for it.\n */\n audio?: MediaTrackConstraints | boolean;\n}\n\n/**\n * Options for {@link captureScreen}.\n *\n * These are merged shallowly over the library defaults: any field you provide\n * replaces the default for that field entirely. Omit a field to keep its\n * default.\n */\nexport interface CaptureScreenOptions {\n /**\n * Video constraints, or `true`. Defaults to a capped width and a low frame\n * rate (see {@link captureScreen}).\n */\n video?: MediaTrackConstraints | boolean;\n /**\n * Audio constraints, or `true`/`false`. Defaults to `false` — screen shares\n * rarely need audio for a video-frame streaming system.\n */\n audio?: MediaTrackConstraints | boolean;\n}\n\n/**\n * Captures the user's camera via `navigator.mediaDevices.getUserMedia`,\n * applying sensible defaults for a video-frame streaming system.\n *\n * Defaults:\n * - `video`: `{ width: { ideal: 1280 }, height: { ideal: 720 }, frameRate: { ideal: 30 } }`\n * — a modest resolution that keeps upload bandwidth reasonable. Cameras are\n * rarely the 4k bandwidth problem that screen capture is, so this is an\n * `ideal` (a hint) rather than a hard cap.\n * - `audio`: `false` — this is a video-frame streaming system.\n *\n * Any option you pass replaces the corresponding default outright (shallow\n * merge), so pass a full `video` constraints object if you want to tweak it.\n *\n * @example\n * ```ts\n * const stream = await captureCamera();\n * await publisher.start(stream);\n * ```\n *\n * @example Front camera with audio:\n * ```ts\n * const stream = await captureCamera({\n * video: { facingMode: \"user\" },\n * audio: true,\n * });\n * ```\n */\nexport async function captureCamera(\n opts: CaptureCameraOptions = {},\n): Promise<MediaStream> {\n const constraints: MediaStreamConstraints = {\n video: opts.video ?? {\n width: { ideal: 1280 },\n height: { ideal: 720 },\n frameRate: { ideal: 30 },\n },\n audio: opts.audio ?? false,\n };\n return navigator.mediaDevices.getUserMedia(constraints);\n}\n\n/**\n * Captures a screen / window / tab via\n * `navigator.mediaDevices.getDisplayMedia`, applying sensible defaults tuned to\n * avoid the HiDPI/Retina bandwidth trap.\n *\n * Defaults:\n * - `video`: `{ width: { max: 1920 }, frameRate: { ideal: 5, max: 10 } }`.\n * The `width` is a **`max`, not an `ideal`**: on a 2x-Retina display the\n * browser would otherwise capture at native resolution (often 3456px+ /\n * effectively 4k), wasting upload bandwidth and downstream decode cost for no\n * visible benefit. Capping the max roughly halves a 2x-Retina share while\n * leaving smaller displays untouched. Screen content is mostly static, so the\n * low frame rate saves further bandwidth.\n * - `audio`: `false`.\n *\n * IMPORTANT — do NOT add `resizeMode: \"none\"` here. That value forbids the\n * browser from downscaling the source, which turns the `width: { max: 1920 }`\n * cap into a no-op on exactly the Retina displays it targets. By omitting\n * `resizeMode` we let the user agent scale to satisfy the constraint (its\n * default behaviour), which is the entire point of this helper. It is tempting\n * to add `resizeMode: \"none\"` back for \"sharpness\" — don't.\n *\n * Any option you pass replaces the corresponding default outright (shallow\n * merge), so pass a full `video` constraints object if you want to tweak it.\n *\n * @example\n * ```ts\n * const stream = await captureScreen();\n * await publisher.start(stream);\n * ```\n */\nexport async function captureScreen(\n opts: CaptureScreenOptions = {},\n): Promise<MediaStream> {\n const constraints: MediaStreamConstraints = {\n video: opts.video ?? {\n width: { max: 1920 },\n frameRate: { ideal: 5, max: 10 },\n },\n audio: opts.audio ?? false,\n };\n return navigator.mediaDevices.getDisplayMedia(constraints);\n}\n"],"mappings":"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","names":[]}