@furious.luke/argus-js 0.2.1 → 0.3.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 CHANGED
@@ -93,6 +93,7 @@ publisher.stop(); // stops all tracks and tears down the peer connection
93
93
  | `gatewayURLs` | `string[]` | **Required.** The `gateway_urls` from the join response. All are raced simultaneously; the fastest to accept wins. |
94
94
  | `token` | `string` | **Required.** The short-lived join token from the join response. |
95
95
  | `iceServers` | `RTCIceServer[]` | Optional extra ICE servers (e.g. your own STUN). TURN is supplied automatically by the winning gateway. |
96
+ | `signalingReconnectTimeoutMs` | `number` | How long to retry a dropped signaling socket against the selected regional gateway. Defaults to 20 seconds. |
96
97
  | `callbacks` | `PublisherCallbacks` | Optional lifecycle callbacks (see below). |
97
98
 
98
99
  ### Methods & properties
@@ -102,7 +103,7 @@ publisher.stop(); // stops all tracks and tears down the peer connection
102
103
  | `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
104
  | `replaceStream(stream)` | Replaces the published tracks and renegotiates in place. |
104
105
  | `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
+ | `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. |
106
107
  | `peerConnection` | The underlying `RTCPeerConnection`, or `null` if not started. |
107
108
  | `isConnected` | `true` when the peer connection state is `"connected"`. |
108
109
 
@@ -112,7 +113,7 @@ publisher.stop(); // stops all tracks and tears down the peer connection
112
113
  | --- | --- |
113
114
  | `onConnected()` | The peer connection reached `"connected"` — media is flowing. |
114
115
  | `onConnectionStateChange(state)` | The `RTCPeerConnectionState` changed. |
115
- | `onError(error)` | A fatal error occurred (signaling error, gateway failure, or closed socket). |
116
+ | `onError(error)` | A fatal error occurred (signaling error, initial gateway failure, or signaling resume timed out). |
116
117
 
117
118
  ## How `start()` works
118
119
 
@@ -120,6 +121,12 @@ publisher.stop(); // stops all tracks and tears down the peer connection
120
121
  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
122
  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
 
124
+ After this initial race, the publisher is pinned to the selected regional
125
+ gateway. If signaling drops, it reconnects to that same gateway with the
126
+ one-hour read token and waits for `resumed`; it does not race regions, rebuild
127
+ the peer connection, or repeat the `ready` handshake. If the retry deadline
128
+ expires, the publisher closes the stream and calls `onError`.
129
+
123
130
  ## Browser support
124
131
 
125
132
  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.
package/dist/index.cjs CHANGED
@@ -73,6 +73,9 @@ function parseSignal(data) {
73
73
  }
74
74
 
75
75
  // src/publisher.ts
76
+ var defaultSignalingReconnectTimeoutMs = 2e4;
77
+ var signalingResumeAttemptTimeoutMs = 3e3;
78
+ var signalingResumeMaxBackoffMs = 3e3;
76
79
  var Publisher = class {
77
80
  opts;
78
81
  sig = null;
@@ -81,10 +84,15 @@ var Publisher = class {
81
84
  pendingRemoteCandidates = [];
82
85
  localStream = null;
83
86
  readToken = null;
87
+ selectedGatewayURL = null;
88
+ stopped = true;
89
+ reconnecting = false;
90
+ reconnectGeneration = 0;
91
+ resumeSocket = null;
84
92
  constructor(opts) {
85
93
  this.opts = opts;
86
94
  }
87
- /** The read token received from the gateway after connecting, for fetching frames. */
95
+ /** The read token used for frame fetches and signaling resume in the selected region. */
88
96
  get frameReadToken() {
89
97
  return this.readToken;
90
98
  }
@@ -95,8 +103,10 @@ var Publisher = class {
95
103
  * use onConnected for that).
96
104
  */
97
105
  async start(stream) {
106
+ this.stopped = false;
98
107
  this.localStream = stream;
99
- const { ws, readyInfo } = await this.raceGateways();
108
+ const { ws, readyInfo, gatewayURL } = await this.raceGateways();
109
+ this.selectedGatewayURL = gatewayURL;
100
110
  if (readyInfo.read_token) {
101
111
  this.readToken = readyInfo.read_token;
102
112
  }
@@ -131,13 +141,10 @@ var Publisher = class {
131
141
  const offer = await this.pc.createOffer();
132
142
  await this.pc.setLocalDescription(offer);
133
143
  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);
144
+ const signaling = this.installSignaling(ws);
138
145
  const local = this.pc.localDescription;
139
146
  if (!local) throw new Error("local description missing after gather");
140
- this.sig.send({ type: "offer", sdp: local.sdp, sdp_type: "offer" });
147
+ signaling.send({ type: "offer", sdp: local.sdp, sdp_type: "offer" });
141
148
  }
142
149
  /** Replaces the currently published stream with a new one. */
143
150
  async replaceStream(stream) {
@@ -165,6 +172,11 @@ var Publisher = class {
165
172
  }
166
173
  /** Stops publishing and tears down the peer connection. */
167
174
  stop() {
175
+ this.stopped = true;
176
+ this.reconnectGeneration++;
177
+ this.reconnecting = false;
178
+ this.resumeSocket?.close();
179
+ this.resumeSocket = null;
168
180
  this.sig?.close();
169
181
  this.sig = null;
170
182
  this.localStream?.getTracks().forEach((t) => t.stop());
@@ -174,6 +186,7 @@ var Publisher = class {
174
186
  this.hasAnswer = false;
175
187
  this.pendingRemoteCandidates = [];
176
188
  this.readToken = null;
189
+ this.selectedGatewayURL = null;
177
190
  }
178
191
  /** Returns the current RTCPeerConnection, or null if not started. */
179
192
  get peerConnection() {
@@ -228,7 +241,7 @@ var Publisher = class {
228
241
  } else if (accepted && msg.type === "ready") {
229
242
  settled = true;
230
243
  closeAll(ws);
231
- resolve({ ws, readyInfo: msg });
244
+ resolve({ ws, readyInfo: msg, gatewayURL });
232
245
  }
233
246
  } catch {
234
247
  }
@@ -238,6 +251,111 @@ var Publisher = class {
238
251
  }
239
252
  });
240
253
  }
254
+ installSignaling(ws) {
255
+ const channel = SignalingChannel.wrap(ws);
256
+ this.sig = channel;
257
+ channel.onMessage = (msg) => this.handleSignal(msg);
258
+ channel.onClose = () => {
259
+ if (this.sig !== channel || this.stopped) return;
260
+ this.sig = null;
261
+ void this.resumeSignaling();
262
+ };
263
+ channel.onError = () => {
264
+ };
265
+ return channel;
266
+ }
267
+ async resumeSignaling() {
268
+ if (this.reconnecting || this.stopped) return;
269
+ if (!this.selectedGatewayURL || !this.readToken) {
270
+ this.terminateWithError(new Error("signaling closed and cannot be resumed"));
271
+ return;
272
+ }
273
+ this.reconnecting = true;
274
+ const generation = ++this.reconnectGeneration;
275
+ const timeout = this.opts.signalingReconnectTimeoutMs ?? defaultSignalingReconnectTimeoutMs;
276
+ const deadline = Date.now() + Math.max(0, timeout);
277
+ let backoffMs = 0;
278
+ while (!this.stopped && generation === this.reconnectGeneration && Date.now() <= deadline) {
279
+ if (backoffMs > 0) {
280
+ const waitMs = Math.min(backoffMs, Math.max(0, deadline - Date.now()));
281
+ if (waitMs === 0) break;
282
+ await this.wait(waitMs);
283
+ if (this.stopped || generation !== this.reconnectGeneration) return;
284
+ }
285
+ const remaining = deadline - Date.now();
286
+ if (remaining < 0) break;
287
+ try {
288
+ const ws = await this.openResumeSocket(Math.min(signalingResumeAttemptTimeoutMs, Math.max(1, remaining)));
289
+ if (this.stopped || generation !== this.reconnectGeneration) {
290
+ ws.close();
291
+ return;
292
+ }
293
+ this.resumeSocket = null;
294
+ this.reconnecting = false;
295
+ this.installSignaling(ws);
296
+ return;
297
+ } catch {
298
+ backoffMs = backoffMs === 0 ? 250 : Math.min(backoffMs * 2, signalingResumeMaxBackoffMs);
299
+ }
300
+ }
301
+ if (!this.stopped && generation === this.reconnectGeneration) {
302
+ this.reconnecting = false;
303
+ this.terminateWithError(new Error("unable to resume signaling with the selected gateway"));
304
+ }
305
+ }
306
+ openResumeSocket(timeoutMs) {
307
+ return new Promise((resolve, reject) => {
308
+ const u = new URL(this.selectedGatewayURL);
309
+ u.searchParams.set("token", this.readToken);
310
+ const ws = new WebSocket(u.toString());
311
+ this.resumeSocket = ws;
312
+ let settled = false;
313
+ const timer = setTimeout(() => fail(), timeoutMs);
314
+ const fail = () => {
315
+ if (settled) return;
316
+ settled = true;
317
+ clearTimeout(timer);
318
+ if (this.resumeSocket === ws) this.resumeSocket = null;
319
+ ws.onmessage = null;
320
+ ws.onerror = null;
321
+ ws.onclose = null;
322
+ ws.close();
323
+ reject(new Error("signaling resume attempt failed"));
324
+ };
325
+ ws.onmessage = (ev) => {
326
+ try {
327
+ const msg = JSON.parse(ev.data);
328
+ if (msg.type !== "resumed" || settled) return;
329
+ settled = true;
330
+ clearTimeout(timer);
331
+ resolve(ws);
332
+ } catch {
333
+ }
334
+ };
335
+ ws.onerror = fail;
336
+ ws.onclose = fail;
337
+ });
338
+ }
339
+ wait(ms) {
340
+ return new Promise((resolve) => setTimeout(resolve, ms));
341
+ }
342
+ terminateWithError(err) {
343
+ this.stopped = true;
344
+ this.reconnectGeneration++;
345
+ this.resumeSocket?.close();
346
+ this.resumeSocket = null;
347
+ this.sig?.close();
348
+ this.sig = null;
349
+ this.pc?.close();
350
+ this.pc = null;
351
+ this.localStream?.getTracks().forEach((track) => track.stop());
352
+ this.localStream = null;
353
+ this.hasAnswer = false;
354
+ this.pendingRemoteCandidates = [];
355
+ this.readToken = null;
356
+ this.selectedGatewayURL = null;
357
+ this.opts.callbacks?.onError?.(err);
358
+ }
241
359
  handleSignal(msg) {
242
360
  switch (msg.type) {
243
361
  case "answer": {
@@ -281,6 +399,8 @@ var Publisher = class {
281
399
  this.opts.callbacks?.onError?.(new Error(msg.error));
282
400
  break;
283
401
  }
402
+ case "resumed":
403
+ break;
284
404
  }
285
405
  }
286
406
  /** Waits for ICE gathering to reach the "complete" state. */
@@ -1 +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. The\n // gateway supplies multi-transport turn_urls (UDP + TCP) so relay can fall\n // back to TCP on UDP-blocking networks. A single credential is valid for\n // every transport.\n const iceServers: RTCIceServer[] = [...(this.opts.iceServers ?? [])];\n if (readyInfo.turn_urls && readyInfo.turn_urls.length > 0) {\n iceServers.push({\n urls: readyInfo.turn_urls,\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 * The `MediaDevices` instance to capture from. Defaults to the global\n * `navigator.mediaDevices`. Pass another window's `navigator.mediaDevices`\n * (e.g. a Document Picture-in-Picture window's) so the browser's permission\n * prompt and transient-activation check are anchored to that window rather\n * than the one holding the global `navigator`.\n */\n mediaDevices?: MediaDevices;\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 * The `MediaDevices` instance to capture from. Defaults to the global\n * `navigator.mediaDevices`. Pass another window's `navigator.mediaDevices`\n * (e.g. a Document Picture-in-Picture window's) so the screen picker and its\n * transient-activation check are anchored to that window rather than the one\n * holding the global `navigator`.\n */\n mediaDevices?: MediaDevices;\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 const mediaDevices = opts.mediaDevices ?? navigator.mediaDevices;\n return 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 const mediaDevices = opts.mediaDevices ?? navigator.mediaDevices;\n return mediaDevices.getDisplayMedia(constraints);\n}\n"],"mappings":"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","names":[]}
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\nconst defaultSignalingReconnectTimeoutMs = 20_000;\nconst signalingResumeAttemptTimeoutMs = 3_000;\nconst signalingResumeMaxBackoffMs = 3_000;\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 private selectedGatewayURL: string | null = null;\n private stopped = true;\n private reconnecting = false;\n private reconnectGeneration = 0;\n private resumeSocket: WebSocket | null = null;\n\n constructor(opts: PublisherOptions) {\n this.opts = opts;\n }\n\n /** The read token used for frame fetches and signaling resume in the selected region. */\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.stopped = false;\n this.localStream = stream;\n\n // Race all gateways; returns winning WebSocket + TURN/read-token info\n const { ws, readyInfo, gatewayURL } = await this.raceGateways();\n this.selectedGatewayURL = gatewayURL;\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. The\n // gateway supplies multi-transport turn_urls (UDP + TCP) so relay can fall\n // back to TCP on UDP-blocking networks. A single credential is valid for\n // every transport.\n const iceServers: RTCIceServer[] = [...(this.opts.iceServers ?? [])];\n if (readyInfo.turn_urls && readyInfo.turn_urls.length > 0) {\n iceServers.push({\n urls: readyInfo.turn_urls,\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 const signaling = this.installSignaling(ws);\n\n const local = this.pc.localDescription;\n if (!local) throw new Error(\"local description missing after gather\");\n signaling.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.stopped = true;\n this.reconnectGeneration++;\n this.reconnecting = false;\n this.resumeSocket?.close();\n this.resumeSocket = null;\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 this.selectedGatewayURL = 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; gatewayURL: string }> {\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, gatewayURL });\n }\n } catch {\n /* ignore malformed */\n }\n };\n\n ws.onerror = () => checkAllFailed();\n ws.onclose = () => checkAllFailed();\n }\n });\n }\n\n private installSignaling(ws: WebSocket): SignalingChannel {\n const channel = SignalingChannel.wrap(ws);\n this.sig = channel;\n channel.onMessage = (msg) => this.handleSignal(msg);\n channel.onClose = () => {\n if (this.sig !== channel || this.stopped) return;\n this.sig = null;\n void this.resumeSignaling();\n };\n // Browsers normally follow an error event with close. Recovery begins from\n // close so a single transport failure cannot start two retry loops.\n channel.onError = () => {};\n return channel;\n }\n\n private async resumeSignaling(): Promise<void> {\n if (this.reconnecting || this.stopped) return;\n if (!this.selectedGatewayURL || !this.readToken) {\n this.terminateWithError(new Error(\"signaling closed and cannot be resumed\"));\n return;\n }\n\n this.reconnecting = true;\n const generation = ++this.reconnectGeneration;\n const timeout = this.opts.signalingReconnectTimeoutMs ?? defaultSignalingReconnectTimeoutMs;\n const deadline = Date.now() + Math.max(0, timeout);\n let backoffMs = 0;\n\n while (!this.stopped && generation === this.reconnectGeneration && Date.now() <= deadline) {\n if (backoffMs > 0) {\n const waitMs = Math.min(backoffMs, Math.max(0, deadline - Date.now()));\n if (waitMs === 0) break;\n await this.wait(waitMs);\n if (this.stopped || generation !== this.reconnectGeneration) return;\n }\n\n const remaining = deadline - Date.now();\n if (remaining < 0) break;\n try {\n const ws = await this.openResumeSocket(Math.min(signalingResumeAttemptTimeoutMs, Math.max(1, remaining)));\n if (this.stopped || generation !== this.reconnectGeneration) {\n ws.close();\n return;\n }\n this.resumeSocket = null;\n this.reconnecting = false;\n this.installSignaling(ws);\n return;\n } catch {\n backoffMs = backoffMs === 0 ? 250 : Math.min(backoffMs * 2, signalingResumeMaxBackoffMs);\n }\n }\n\n if (!this.stopped && generation === this.reconnectGeneration) {\n this.reconnecting = false;\n this.terminateWithError(new Error(\"unable to resume signaling with the selected gateway\"));\n }\n }\n\n private openResumeSocket(timeoutMs: number): Promise<WebSocket> {\n return new Promise((resolve, reject) => {\n const u = new URL(this.selectedGatewayURL!);\n u.searchParams.set(\"token\", this.readToken!);\n const ws = new WebSocket(u.toString());\n this.resumeSocket = ws;\n let settled = false;\n const timer = setTimeout(() => fail(), timeoutMs);\n\n const fail = () => {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n if (this.resumeSocket === ws) this.resumeSocket = null;\n ws.onmessage = null;\n ws.onerror = null;\n ws.onclose = null;\n ws.close();\n reject(new Error(\"signaling resume attempt failed\"));\n };\n\n ws.onmessage = (ev: MessageEvent) => {\n try {\n const msg = JSON.parse(ev.data as string);\n if (msg.type !== \"resumed\" || settled) return;\n settled = true;\n clearTimeout(timer);\n resolve(ws);\n } catch {\n // Ignore malformed messages while waiting for the resume acknowledgement.\n }\n };\n ws.onerror = fail;\n ws.onclose = fail;\n });\n }\n\n private wait(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n }\n\n private terminateWithError(err: Error): void {\n this.stopped = true;\n this.reconnectGeneration++;\n this.resumeSocket?.close();\n this.resumeSocket = null;\n this.sig?.close();\n this.sig = null;\n this.pc?.close();\n this.pc = null;\n this.localStream?.getTracks().forEach((track) => track.stop());\n this.localStream = null;\n this.hasAnswer = false;\n this.pendingRemoteCandidates = [];\n this.readToken = null;\n this.selectedGatewayURL = null;\n this.opts.callbacks?.onError?.(err);\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 case \"resumed\":\n break;\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 * The `MediaDevices` instance to capture from. Defaults to the global\n * `navigator.mediaDevices`. Pass another window's `navigator.mediaDevices`\n * (e.g. a Document Picture-in-Picture window's) so the browser's permission\n * prompt and transient-activation check are anchored to that window rather\n * than the one holding the global `navigator`.\n */\n mediaDevices?: MediaDevices;\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 * The `MediaDevices` instance to capture from. Defaults to the global\n * `navigator.mediaDevices`. Pass another window's `navigator.mediaDevices`\n * (e.g. a Document Picture-in-Picture window's) so the screen picker and its\n * transient-activation check are anchored to that window rather than the one\n * holding the global `navigator`.\n */\n mediaDevices?: MediaDevices;\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 const mediaDevices = opts.mediaDevices ?? navigator.mediaDevices;\n return 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 const mediaDevices = opts.mediaDevices ?? navigator.mediaDevices;\n return mediaDevices.getDisplayMedia(constraints);\n}\n"],"mappings":"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","names":[]}
package/dist/index.d.cts CHANGED
@@ -21,6 +21,8 @@ type SignalMessage = {
21
21
  } | {
22
22
  type: "error";
23
23
  error: string;
24
+ } | {
25
+ type: "resumed";
24
26
  } | {
25
27
  type: "accepted";
26
28
  } | {
@@ -38,7 +40,7 @@ type SignalMessage = {
38
40
  interface PublisherCallbacks {
39
41
  /** Called when the WebRTC peer connection state changes. */
40
42
  onConnectionStateChange?: (state: RTCPeerConnectionState) => void;
41
- /** Called when a fatal error occurs (e.g. signaling error, WebSocket close). */
43
+ /** Called when a fatal error occurs, including when signaling cannot resume before its deadline. */
42
44
  onError?: (error: Error) => void;
43
45
  /** Called when the browser has successfully connected to the media server. */
44
46
  onConnected?: () => void;
@@ -51,6 +53,7 @@ interface GatewayReadyInfo {
51
53
  turn_urls?: string[];
52
54
  turn_username?: string;
53
55
  turn_credential?: string;
56
+ /** One-hour token used for frame reads and resuming signaling in the selected region. */
54
57
  read_token?: string;
55
58
  }
56
59
  /**
@@ -63,6 +66,8 @@ interface PublisherOptions {
63
66
  token: string;
64
67
  /** Optional extra ICE servers (e.g. STUN). TURN is supplied by the winning gateway. */
65
68
  iceServers?: RTCIceServer[];
69
+ /** How long to retry a dropped signaling connection to the selected gateway. Defaults to 20 seconds. */
70
+ signalingReconnectTimeoutMs?: number;
66
71
  /** Callbacks for lifecycle events. */
67
72
  callbacks?: PublisherCallbacks;
68
73
  }
@@ -97,8 +102,13 @@ declare class Publisher {
97
102
  private pendingRemoteCandidates;
98
103
  private localStream;
99
104
  private readToken;
105
+ private selectedGatewayURL;
106
+ private stopped;
107
+ private reconnecting;
108
+ private reconnectGeneration;
109
+ private resumeSocket;
100
110
  constructor(opts: PublisherOptions);
101
- /** The read token received from the gateway after connecting, for fetching frames. */
111
+ /** The read token used for frame fetches and signaling resume in the selected region. */
102
112
  get frameReadToken(): string | null;
103
113
  /**
104
114
  * Starts the publisher: races all gateways to find the fastest, completes
@@ -116,6 +126,11 @@ declare class Publisher {
116
126
  /** Returns true if the peer connection is in the "connected" state. */
117
127
  get isConnected(): boolean;
118
128
  private raceGateways;
129
+ private installSignaling;
130
+ private resumeSignaling;
131
+ private openResumeSocket;
132
+ private wait;
133
+ private terminateWithError;
119
134
  private handleSignal;
120
135
  /** Waits for ICE gathering to reach the "complete" state. */
121
136
  private gatherComplete;
package/dist/index.d.ts CHANGED
@@ -21,6 +21,8 @@ type SignalMessage = {
21
21
  } | {
22
22
  type: "error";
23
23
  error: string;
24
+ } | {
25
+ type: "resumed";
24
26
  } | {
25
27
  type: "accepted";
26
28
  } | {
@@ -38,7 +40,7 @@ type SignalMessage = {
38
40
  interface PublisherCallbacks {
39
41
  /** Called when the WebRTC peer connection state changes. */
40
42
  onConnectionStateChange?: (state: RTCPeerConnectionState) => void;
41
- /** Called when a fatal error occurs (e.g. signaling error, WebSocket close). */
43
+ /** Called when a fatal error occurs, including when signaling cannot resume before its deadline. */
42
44
  onError?: (error: Error) => void;
43
45
  /** Called when the browser has successfully connected to the media server. */
44
46
  onConnected?: () => void;
@@ -51,6 +53,7 @@ interface GatewayReadyInfo {
51
53
  turn_urls?: string[];
52
54
  turn_username?: string;
53
55
  turn_credential?: string;
56
+ /** One-hour token used for frame reads and resuming signaling in the selected region. */
54
57
  read_token?: string;
55
58
  }
56
59
  /**
@@ -63,6 +66,8 @@ interface PublisherOptions {
63
66
  token: string;
64
67
  /** Optional extra ICE servers (e.g. STUN). TURN is supplied by the winning gateway. */
65
68
  iceServers?: RTCIceServer[];
69
+ /** How long to retry a dropped signaling connection to the selected gateway. Defaults to 20 seconds. */
70
+ signalingReconnectTimeoutMs?: number;
66
71
  /** Callbacks for lifecycle events. */
67
72
  callbacks?: PublisherCallbacks;
68
73
  }
@@ -97,8 +102,13 @@ declare class Publisher {
97
102
  private pendingRemoteCandidates;
98
103
  private localStream;
99
104
  private readToken;
105
+ private selectedGatewayURL;
106
+ private stopped;
107
+ private reconnecting;
108
+ private reconnectGeneration;
109
+ private resumeSocket;
100
110
  constructor(opts: PublisherOptions);
101
- /** The read token received from the gateway after connecting, for fetching frames. */
111
+ /** The read token used for frame fetches and signaling resume in the selected region. */
102
112
  get frameReadToken(): string | null;
103
113
  /**
104
114
  * Starts the publisher: races all gateways to find the fastest, completes
@@ -116,6 +126,11 @@ declare class Publisher {
116
126
  /** Returns true if the peer connection is in the "connected" state. */
117
127
  get isConnected(): boolean;
118
128
  private raceGateways;
129
+ private installSignaling;
130
+ private resumeSignaling;
131
+ private openResumeSocket;
132
+ private wait;
133
+ private terminateWithError;
119
134
  private handleSignal;
120
135
  /** Waits for ICE gathering to reach the "complete" state. */
121
136
  private gatherComplete;
package/dist/index.js CHANGED
@@ -45,6 +45,9 @@ function parseSignal(data) {
45
45
  }
46
46
 
47
47
  // src/publisher.ts
48
+ var defaultSignalingReconnectTimeoutMs = 2e4;
49
+ var signalingResumeAttemptTimeoutMs = 3e3;
50
+ var signalingResumeMaxBackoffMs = 3e3;
48
51
  var Publisher = class {
49
52
  opts;
50
53
  sig = null;
@@ -53,10 +56,15 @@ var Publisher = class {
53
56
  pendingRemoteCandidates = [];
54
57
  localStream = null;
55
58
  readToken = null;
59
+ selectedGatewayURL = null;
60
+ stopped = true;
61
+ reconnecting = false;
62
+ reconnectGeneration = 0;
63
+ resumeSocket = null;
56
64
  constructor(opts) {
57
65
  this.opts = opts;
58
66
  }
59
- /** The read token received from the gateway after connecting, for fetching frames. */
67
+ /** The read token used for frame fetches and signaling resume in the selected region. */
60
68
  get frameReadToken() {
61
69
  return this.readToken;
62
70
  }
@@ -67,8 +75,10 @@ var Publisher = class {
67
75
  * use onConnected for that).
68
76
  */
69
77
  async start(stream) {
78
+ this.stopped = false;
70
79
  this.localStream = stream;
71
- const { ws, readyInfo } = await this.raceGateways();
80
+ const { ws, readyInfo, gatewayURL } = await this.raceGateways();
81
+ this.selectedGatewayURL = gatewayURL;
72
82
  if (readyInfo.read_token) {
73
83
  this.readToken = readyInfo.read_token;
74
84
  }
@@ -103,13 +113,10 @@ var Publisher = class {
103
113
  const offer = await this.pc.createOffer();
104
114
  await this.pc.setLocalDescription(offer);
105
115
  await this.gatherComplete();
106
- this.sig = SignalingChannel.wrap(ws);
107
- this.sig.onMessage = (msg) => this.handleSignal(msg);
108
- this.sig.onClose = () => this.opts.callbacks?.onError?.(new Error("signaling closed"));
109
- this.sig.onError = (err) => this.opts.callbacks?.onError?.(err);
116
+ const signaling = this.installSignaling(ws);
110
117
  const local = this.pc.localDescription;
111
118
  if (!local) throw new Error("local description missing after gather");
112
- this.sig.send({ type: "offer", sdp: local.sdp, sdp_type: "offer" });
119
+ signaling.send({ type: "offer", sdp: local.sdp, sdp_type: "offer" });
113
120
  }
114
121
  /** Replaces the currently published stream with a new one. */
115
122
  async replaceStream(stream) {
@@ -137,6 +144,11 @@ var Publisher = class {
137
144
  }
138
145
  /** Stops publishing and tears down the peer connection. */
139
146
  stop() {
147
+ this.stopped = true;
148
+ this.reconnectGeneration++;
149
+ this.reconnecting = false;
150
+ this.resumeSocket?.close();
151
+ this.resumeSocket = null;
140
152
  this.sig?.close();
141
153
  this.sig = null;
142
154
  this.localStream?.getTracks().forEach((t) => t.stop());
@@ -146,6 +158,7 @@ var Publisher = class {
146
158
  this.hasAnswer = false;
147
159
  this.pendingRemoteCandidates = [];
148
160
  this.readToken = null;
161
+ this.selectedGatewayURL = null;
149
162
  }
150
163
  /** Returns the current RTCPeerConnection, or null if not started. */
151
164
  get peerConnection() {
@@ -200,7 +213,7 @@ var Publisher = class {
200
213
  } else if (accepted && msg.type === "ready") {
201
214
  settled = true;
202
215
  closeAll(ws);
203
- resolve({ ws, readyInfo: msg });
216
+ resolve({ ws, readyInfo: msg, gatewayURL });
204
217
  }
205
218
  } catch {
206
219
  }
@@ -210,6 +223,111 @@ var Publisher = class {
210
223
  }
211
224
  });
212
225
  }
226
+ installSignaling(ws) {
227
+ const channel = SignalingChannel.wrap(ws);
228
+ this.sig = channel;
229
+ channel.onMessage = (msg) => this.handleSignal(msg);
230
+ channel.onClose = () => {
231
+ if (this.sig !== channel || this.stopped) return;
232
+ this.sig = null;
233
+ void this.resumeSignaling();
234
+ };
235
+ channel.onError = () => {
236
+ };
237
+ return channel;
238
+ }
239
+ async resumeSignaling() {
240
+ if (this.reconnecting || this.stopped) return;
241
+ if (!this.selectedGatewayURL || !this.readToken) {
242
+ this.terminateWithError(new Error("signaling closed and cannot be resumed"));
243
+ return;
244
+ }
245
+ this.reconnecting = true;
246
+ const generation = ++this.reconnectGeneration;
247
+ const timeout = this.opts.signalingReconnectTimeoutMs ?? defaultSignalingReconnectTimeoutMs;
248
+ const deadline = Date.now() + Math.max(0, timeout);
249
+ let backoffMs = 0;
250
+ while (!this.stopped && generation === this.reconnectGeneration && Date.now() <= deadline) {
251
+ if (backoffMs > 0) {
252
+ const waitMs = Math.min(backoffMs, Math.max(0, deadline - Date.now()));
253
+ if (waitMs === 0) break;
254
+ await this.wait(waitMs);
255
+ if (this.stopped || generation !== this.reconnectGeneration) return;
256
+ }
257
+ const remaining = deadline - Date.now();
258
+ if (remaining < 0) break;
259
+ try {
260
+ const ws = await this.openResumeSocket(Math.min(signalingResumeAttemptTimeoutMs, Math.max(1, remaining)));
261
+ if (this.stopped || generation !== this.reconnectGeneration) {
262
+ ws.close();
263
+ return;
264
+ }
265
+ this.resumeSocket = null;
266
+ this.reconnecting = false;
267
+ this.installSignaling(ws);
268
+ return;
269
+ } catch {
270
+ backoffMs = backoffMs === 0 ? 250 : Math.min(backoffMs * 2, signalingResumeMaxBackoffMs);
271
+ }
272
+ }
273
+ if (!this.stopped && generation === this.reconnectGeneration) {
274
+ this.reconnecting = false;
275
+ this.terminateWithError(new Error("unable to resume signaling with the selected gateway"));
276
+ }
277
+ }
278
+ openResumeSocket(timeoutMs) {
279
+ return new Promise((resolve, reject) => {
280
+ const u = new URL(this.selectedGatewayURL);
281
+ u.searchParams.set("token", this.readToken);
282
+ const ws = new WebSocket(u.toString());
283
+ this.resumeSocket = ws;
284
+ let settled = false;
285
+ const timer = setTimeout(() => fail(), timeoutMs);
286
+ const fail = () => {
287
+ if (settled) return;
288
+ settled = true;
289
+ clearTimeout(timer);
290
+ if (this.resumeSocket === ws) this.resumeSocket = null;
291
+ ws.onmessage = null;
292
+ ws.onerror = null;
293
+ ws.onclose = null;
294
+ ws.close();
295
+ reject(new Error("signaling resume attempt failed"));
296
+ };
297
+ ws.onmessage = (ev) => {
298
+ try {
299
+ const msg = JSON.parse(ev.data);
300
+ if (msg.type !== "resumed" || settled) return;
301
+ settled = true;
302
+ clearTimeout(timer);
303
+ resolve(ws);
304
+ } catch {
305
+ }
306
+ };
307
+ ws.onerror = fail;
308
+ ws.onclose = fail;
309
+ });
310
+ }
311
+ wait(ms) {
312
+ return new Promise((resolve) => setTimeout(resolve, ms));
313
+ }
314
+ terminateWithError(err) {
315
+ this.stopped = true;
316
+ this.reconnectGeneration++;
317
+ this.resumeSocket?.close();
318
+ this.resumeSocket = null;
319
+ this.sig?.close();
320
+ this.sig = null;
321
+ this.pc?.close();
322
+ this.pc = null;
323
+ this.localStream?.getTracks().forEach((track) => track.stop());
324
+ this.localStream = null;
325
+ this.hasAnswer = false;
326
+ this.pendingRemoteCandidates = [];
327
+ this.readToken = null;
328
+ this.selectedGatewayURL = null;
329
+ this.opts.callbacks?.onError?.(err);
330
+ }
213
331
  handleSignal(msg) {
214
332
  switch (msg.type) {
215
333
  case "answer": {
@@ -253,6 +371,8 @@ var Publisher = class {
253
371
  this.opts.callbacks?.onError?.(new Error(msg.error));
254
372
  break;
255
373
  }
374
+ case "resumed":
375
+ break;
256
376
  }
257
377
  }
258
378
  /** Waits for ICE gathering to reach the "complete" state. */
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/signaling.ts","../src/publisher.ts","../src/capture.ts"],"sourcesContent":["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. The\n // gateway supplies multi-transport turn_urls (UDP + TCP) so relay can fall\n // back to TCP on UDP-blocking networks. A single credential is valid for\n // every transport.\n const iceServers: RTCIceServer[] = [...(this.opts.iceServers ?? [])];\n if (readyInfo.turn_urls && readyInfo.turn_urls.length > 0) {\n iceServers.push({\n urls: readyInfo.turn_urls,\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 * The `MediaDevices` instance to capture from. Defaults to the global\n * `navigator.mediaDevices`. Pass another window's `navigator.mediaDevices`\n * (e.g. a Document Picture-in-Picture window's) so the browser's permission\n * prompt and transient-activation check are anchored to that window rather\n * than the one holding the global `navigator`.\n */\n mediaDevices?: MediaDevices;\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 * The `MediaDevices` instance to capture from. Defaults to the global\n * `navigator.mediaDevices`. Pass another window's `navigator.mediaDevices`\n * (e.g. a Document Picture-in-Picture window's) so the screen picker and its\n * transient-activation check are anchored to that window rather than the one\n * holding the global `navigator`.\n */\n mediaDevices?: MediaDevices;\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 const mediaDevices = opts.mediaDevices ?? navigator.mediaDevices;\n return 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 const mediaDevices = opts.mediaDevices ?? navigator.mediaDevices;\n return mediaDevices.getDisplayMedia(constraints);\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/signaling.ts","../src/publisher.ts","../src/capture.ts"],"sourcesContent":["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\nconst defaultSignalingReconnectTimeoutMs = 20_000;\nconst signalingResumeAttemptTimeoutMs = 3_000;\nconst signalingResumeMaxBackoffMs = 3_000;\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 private selectedGatewayURL: string | null = null;\n private stopped = true;\n private reconnecting = false;\n private reconnectGeneration = 0;\n private resumeSocket: WebSocket | null = null;\n\n constructor(opts: PublisherOptions) {\n this.opts = opts;\n }\n\n /** The read token used for frame fetches and signaling resume in the selected region. */\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.stopped = false;\n this.localStream = stream;\n\n // Race all gateways; returns winning WebSocket + TURN/read-token info\n const { ws, readyInfo, gatewayURL } = await this.raceGateways();\n this.selectedGatewayURL = gatewayURL;\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. The\n // gateway supplies multi-transport turn_urls (UDP + TCP) so relay can fall\n // back to TCP on UDP-blocking networks. A single credential is valid for\n // every transport.\n const iceServers: RTCIceServer[] = [...(this.opts.iceServers ?? [])];\n if (readyInfo.turn_urls && readyInfo.turn_urls.length > 0) {\n iceServers.push({\n urls: readyInfo.turn_urls,\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 const signaling = this.installSignaling(ws);\n\n const local = this.pc.localDescription;\n if (!local) throw new Error(\"local description missing after gather\");\n signaling.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.stopped = true;\n this.reconnectGeneration++;\n this.reconnecting = false;\n this.resumeSocket?.close();\n this.resumeSocket = null;\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 this.selectedGatewayURL = 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; gatewayURL: string }> {\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, gatewayURL });\n }\n } catch {\n /* ignore malformed */\n }\n };\n\n ws.onerror = () => checkAllFailed();\n ws.onclose = () => checkAllFailed();\n }\n });\n }\n\n private installSignaling(ws: WebSocket): SignalingChannel {\n const channel = SignalingChannel.wrap(ws);\n this.sig = channel;\n channel.onMessage = (msg) => this.handleSignal(msg);\n channel.onClose = () => {\n if (this.sig !== channel || this.stopped) return;\n this.sig = null;\n void this.resumeSignaling();\n };\n // Browsers normally follow an error event with close. Recovery begins from\n // close so a single transport failure cannot start two retry loops.\n channel.onError = () => {};\n return channel;\n }\n\n private async resumeSignaling(): Promise<void> {\n if (this.reconnecting || this.stopped) return;\n if (!this.selectedGatewayURL || !this.readToken) {\n this.terminateWithError(new Error(\"signaling closed and cannot be resumed\"));\n return;\n }\n\n this.reconnecting = true;\n const generation = ++this.reconnectGeneration;\n const timeout = this.opts.signalingReconnectTimeoutMs ?? defaultSignalingReconnectTimeoutMs;\n const deadline = Date.now() + Math.max(0, timeout);\n let backoffMs = 0;\n\n while (!this.stopped && generation === this.reconnectGeneration && Date.now() <= deadline) {\n if (backoffMs > 0) {\n const waitMs = Math.min(backoffMs, Math.max(0, deadline - Date.now()));\n if (waitMs === 0) break;\n await this.wait(waitMs);\n if (this.stopped || generation !== this.reconnectGeneration) return;\n }\n\n const remaining = deadline - Date.now();\n if (remaining < 0) break;\n try {\n const ws = await this.openResumeSocket(Math.min(signalingResumeAttemptTimeoutMs, Math.max(1, remaining)));\n if (this.stopped || generation !== this.reconnectGeneration) {\n ws.close();\n return;\n }\n this.resumeSocket = null;\n this.reconnecting = false;\n this.installSignaling(ws);\n return;\n } catch {\n backoffMs = backoffMs === 0 ? 250 : Math.min(backoffMs * 2, signalingResumeMaxBackoffMs);\n }\n }\n\n if (!this.stopped && generation === this.reconnectGeneration) {\n this.reconnecting = false;\n this.terminateWithError(new Error(\"unable to resume signaling with the selected gateway\"));\n }\n }\n\n private openResumeSocket(timeoutMs: number): Promise<WebSocket> {\n return new Promise((resolve, reject) => {\n const u = new URL(this.selectedGatewayURL!);\n u.searchParams.set(\"token\", this.readToken!);\n const ws = new WebSocket(u.toString());\n this.resumeSocket = ws;\n let settled = false;\n const timer = setTimeout(() => fail(), timeoutMs);\n\n const fail = () => {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n if (this.resumeSocket === ws) this.resumeSocket = null;\n ws.onmessage = null;\n ws.onerror = null;\n ws.onclose = null;\n ws.close();\n reject(new Error(\"signaling resume attempt failed\"));\n };\n\n ws.onmessage = (ev: MessageEvent) => {\n try {\n const msg = JSON.parse(ev.data as string);\n if (msg.type !== \"resumed\" || settled) return;\n settled = true;\n clearTimeout(timer);\n resolve(ws);\n } catch {\n // Ignore malformed messages while waiting for the resume acknowledgement.\n }\n };\n ws.onerror = fail;\n ws.onclose = fail;\n });\n }\n\n private wait(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n }\n\n private terminateWithError(err: Error): void {\n this.stopped = true;\n this.reconnectGeneration++;\n this.resumeSocket?.close();\n this.resumeSocket = null;\n this.sig?.close();\n this.sig = null;\n this.pc?.close();\n this.pc = null;\n this.localStream?.getTracks().forEach((track) => track.stop());\n this.localStream = null;\n this.hasAnswer = false;\n this.pendingRemoteCandidates = [];\n this.readToken = null;\n this.selectedGatewayURL = null;\n this.opts.callbacks?.onError?.(err);\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 case \"resumed\":\n break;\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 * The `MediaDevices` instance to capture from. Defaults to the global\n * `navigator.mediaDevices`. Pass another window's `navigator.mediaDevices`\n * (e.g. a Document Picture-in-Picture window's) so the browser's permission\n * prompt and transient-activation check are anchored to that window rather\n * than the one holding the global `navigator`.\n */\n mediaDevices?: MediaDevices;\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 * The `MediaDevices` instance to capture from. Defaults to the global\n * `navigator.mediaDevices`. Pass another window's `navigator.mediaDevices`\n * (e.g. a Document Picture-in-Picture window's) so the screen picker and its\n * transient-activation check are anchored to that window rather than the one\n * holding the global `navigator`.\n */\n mediaDevices?: MediaDevices;\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 const mediaDevices = opts.mediaDevices ?? navigator.mediaDevices;\n return 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 const mediaDevices = opts.mediaDevices ?? navigator.mediaDevices;\n return mediaDevices.getDisplayMedia(constraints);\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@furious.luke/argus-js",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Browser client for the Argus video streaming platform — publish WebRTC video to Argus media servers.",
5
5
  "keywords": [
6
6
  "argus",