@furious.luke/argus-js 0.3.0 → 0.4.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
@@ -10,7 +10,8 @@ Argus is a distributed video ingestion service. A stream flows through three par
10
10
 
11
11
  - **Your server** mints a short-lived **join token** using its secret API key (see the [Go client](https://github.com/furious-luke/go-projects/tree/main/argus/client), published as the `github.com/furious-luke/argus-go` module, or the control-plane HTTP API).
12
12
  - **The browser** (this library) uses that token to publish video to the nearest Argus media server. It never sees the API key.
13
- - **Your server** later fetches frames from the stream on demand.
13
+ - **The browser** relays the winning gateway URL and its region-scoped read token
14
+ back to your server, which uses that pair for frame reads and notifications.
14
15
 
15
16
  This library is only the browser publishing half. It deliberately does **not** talk to the control plane or mint tokens — that requires your secret API key and must stay server-side.
16
17
 
@@ -21,6 +22,7 @@ This library is only the browser publishing half. It deliberately does **not** t
21
22
  │ argus-js │ 2. token + gateway_urls └────────────┘
22
23
  │ │
23
24
  │ │ 3. publish WebRTC video ────────────────▶ Argus media server
25
+ │ │ 4. frameReadToken + selectedGatewayURL ─▶ your server
24
26
  └──────────┘
25
27
  ```
26
28
 
@@ -42,8 +44,24 @@ const publisher = new Publisher({
42
44
  gatewayURLs: join.gateway_urls,
43
45
  token: join.token,
44
46
  callbacks: {
45
- onConnected: () => console.log("streaming live"),
47
+ onConnected: async () => {
48
+ console.log("streaming live");
49
+ // Relay these as a pair; the read token is valid in the selected region.
50
+ await fetch("/api/stream-credentials", {
51
+ method: "POST",
52
+ headers: { "Content-Type": "application/json" },
53
+ body: JSON.stringify({
54
+ stream_id: join.stream_id,
55
+ read_token: publisher.frameReadToken,
56
+ gateway_url: publisher.selectedGatewayURL,
57
+ }),
58
+ });
59
+ },
46
60
  onConnectionStateChange: (state) => console.log("state:", state),
61
+ onRecoveryStateChange: (event) => console.log("media recovery:", event),
62
+ // Show a button or prompt. From its click handler, call captureScreen()
63
+ // and then publisher.replaceStream(newStream).
64
+ onRecoveryRequired: (event) => console.warn("screen share must be restarted", event),
47
65
  onError: (err) => console.error("publish error:", err),
48
66
  },
49
67
  });
@@ -51,9 +69,6 @@ const publisher = new Publisher({
51
69
  // captureCamera() applies sensible capture defaults; any MediaStream works too.
52
70
  const stream = await captureCamera();
53
71
  await publisher.start(stream);
54
-
55
- // After start() resolves, this token lets your server fetch frames for the stream.
56
- console.log("read token:", publisher.frameReadToken);
57
72
  ```
58
73
 
59
74
  ### Capture helpers
@@ -104,6 +119,7 @@ publisher.stop(); // stops all tracks and tears down the peer connection
104
119
  | `replaceStream(stream)` | Replaces the published tracks and renegotiates in place. |
105
120
  | `stop()` | Stops all local tracks and closes the peer connection. |
106
121
  | `frameReadToken` | The one-hour read token from the gateway's `ready` message, or `null` before connecting. The publisher uses it internally to resume signaling; hand it to your server to fetch frames. |
122
+ | `selectedGatewayURL` | The signaling URL that won the regional race, or `null` before connecting. Relay it with `frameReadToken` so your server uses the same region for frame reads and notifications. |
107
123
  | `peerConnection` | The underlying `RTCPeerConnection`, or `null` if not started. |
108
124
  | `isConnected` | `true` when the peer connection state is `"connected"`. |
109
125
 
@@ -113,6 +129,8 @@ publisher.stop(); // stops all tracks and tears down the peer connection
113
129
  | --- | --- |
114
130
  | `onConnected()` | The peer connection reached `"connected"` — media is flowing. |
115
131
  | `onConnectionStateChange(state)` | The `RTCPeerConnectionState` changed. |
132
+ | `onRecoveryStateChange(event)` | Argus detected stalled media and the publisher started, escalated, completed, or failed automatic recovery. |
133
+ | `onRecoveryRequired(event)` | Automatic recovery could not restore media, or capture ended and the host must ask the user for a new screen share. |
116
134
  | `onError(error)` | A fatal error occurred (signaling error, initial gateway failure, or signaling resume timed out). |
117
135
 
118
136
  ## How `start()` works
@@ -127,6 +145,22 @@ one-hour read token and waits for `resumed`; it does not race regions, rebuild
127
145
  the peer connection, or repeat the `ready` handshake. If the retry deadline
128
146
  expires, the publisher closes the stream and calls `onError`.
129
147
 
148
+ ## Automatic media recovery
149
+
150
+ Argus measures freshness from complete encoded samples received by the media
151
+ server, not from pixel changes. An unchanged screen therefore remains healthy.
152
+ If samples stop for 15 seconds, the media server tells the publisher that the
153
+ track has stalled and stops serving its retained frame as current. The publisher
154
+ first detaches and reattaches the live sender and renegotiates. If samples do not
155
+ resume within four seconds, it performs an ICE restart and waits another eight
156
+ seconds. These fixed timings deliberately are not application configuration.
157
+
158
+ `onRecoveryStateChange` reports the recovery transitions. If both automatic
159
+ steps fail, `onRecoveryRequired` is called and the host can acquire a replacement
160
+ stream and pass it to `replaceStream`. A browser cannot silently reacquire a
161
+ screen share after the user or operating system ends it, so `capture_ended`
162
+ always requires host UI and a fresh `captureScreen()` call.
163
+
130
164
  ## Browser support
131
165
 
132
166
  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
@@ -76,6 +76,9 @@ function parseSignal(data) {
76
76
  var defaultSignalingReconnectTimeoutMs = 2e4;
77
77
  var signalingResumeAttemptTimeoutMs = 3e3;
78
78
  var signalingResumeMaxBackoffMs = 3e3;
79
+ var senderRestartPauseMs = 100;
80
+ var senderRecoveryWaitMs = 4e3;
81
+ var iceRecoveryWaitMs = 8e3;
79
82
  var Publisher = class {
80
83
  opts;
81
84
  sig = null;
@@ -84,11 +87,16 @@ var Publisher = class {
84
87
  pendingRemoteCandidates = [];
85
88
  localStream = null;
86
89
  readToken = null;
87
- selectedGatewayURL = null;
90
+ gatewayURL = null;
88
91
  stopped = true;
89
92
  reconnecting = false;
90
93
  reconnectGeneration = 0;
91
94
  resumeSocket = null;
95
+ recoveryGeneration = 0;
96
+ recoveringMedia = false;
97
+ recoveryRequired = false;
98
+ recoveryAction = null;
99
+ trackEndHandlers = /* @__PURE__ */ new Map();
92
100
  constructor(opts) {
93
101
  this.opts = opts;
94
102
  }
@@ -96,6 +104,14 @@ var Publisher = class {
96
104
  get frameReadToken() {
97
105
  return this.readToken;
98
106
  }
107
+ /**
108
+ * The signaling gateway URL that won the initial race, or null before start.
109
+ * Relay this with frameReadToken so the application server can reach the same
110
+ * region for frame reads and change-notification subscriptions.
111
+ */
112
+ get selectedGatewayURL() {
113
+ return this.gatewayURL;
114
+ }
99
115
  /**
100
116
  * Starts the publisher: races all gateways to find the fastest, completes
101
117
  * the two-phase handshake, creates the peer connection, and sends the SDP
@@ -104,9 +120,11 @@ var Publisher = class {
104
120
  */
105
121
  async start(stream) {
106
122
  this.stopped = false;
123
+ this.recoveryRequired = false;
107
124
  this.localStream = stream;
125
+ this.watchStreamTracks(stream);
108
126
  const { ws, readyInfo, gatewayURL } = await this.raceGateways();
109
- this.selectedGatewayURL = gatewayURL;
127
+ this.gatewayURL = gatewayURL;
110
128
  if (readyInfo.read_token) {
111
129
  this.readToken = readyInfo.read_token;
112
130
  }
@@ -149,6 +167,9 @@ var Publisher = class {
149
167
  /** Replaces the currently published stream with a new one. */
150
168
  async replaceStream(stream) {
151
169
  if (!this.pc) throw new Error("publisher not started");
170
+ this.cancelMediaRecovery();
171
+ this.recoveryRequired = false;
172
+ this.unwatchStreamTracks();
152
173
  const senders = this.pc.getSenders();
153
174
  for (const sender of senders) {
154
175
  if (sender.track) {
@@ -159,26 +180,20 @@ var Publisher = class {
159
180
  this.pc.addTrack(track, stream);
160
181
  }
161
182
  this.localStream = stream;
162
- const offer = await this.pc.createOffer();
163
- await this.pc.setLocalDescription(offer);
164
- await this.gatherComplete();
165
- const local = this.pc.localDescription;
166
- if (!local) throw new Error("local description missing");
167
- this.sig?.send({
168
- type: "offer",
169
- sdp: local.sdp,
170
- sdp_type: "offer"
171
- });
183
+ this.watchStreamTracks(stream);
184
+ await this.renegotiate(false);
172
185
  }
173
186
  /** Stops publishing and tears down the peer connection. */
174
187
  stop() {
175
188
  this.stopped = true;
189
+ this.cancelMediaRecovery();
176
190
  this.reconnectGeneration++;
177
191
  this.reconnecting = false;
178
192
  this.resumeSocket?.close();
179
193
  this.resumeSocket = null;
180
194
  this.sig?.close();
181
195
  this.sig = null;
196
+ this.unwatchStreamTracks();
182
197
  this.localStream?.getTracks().forEach((t) => t.stop());
183
198
  this.localStream = null;
184
199
  this.pc?.close();
@@ -186,7 +201,7 @@ var Publisher = class {
186
201
  this.hasAnswer = false;
187
202
  this.pendingRemoteCandidates = [];
188
203
  this.readToken = null;
189
- this.selectedGatewayURL = null;
204
+ this.gatewayURL = null;
190
205
  }
191
206
  /** Returns the current RTCPeerConnection, or null if not started. */
192
207
  get peerConnection() {
@@ -266,7 +281,7 @@ var Publisher = class {
266
281
  }
267
282
  async resumeSignaling() {
268
283
  if (this.reconnecting || this.stopped) return;
269
- if (!this.selectedGatewayURL || !this.readToken) {
284
+ if (!this.gatewayURL || !this.readToken) {
270
285
  this.terminateWithError(new Error("signaling closed and cannot be resumed"));
271
286
  return;
272
287
  }
@@ -305,7 +320,7 @@ var Publisher = class {
305
320
  }
306
321
  openResumeSocket(timeoutMs) {
307
322
  return new Promise((resolve, reject) => {
308
- const u = new URL(this.selectedGatewayURL);
323
+ const u = new URL(this.gatewayURL);
309
324
  u.searchParams.set("token", this.readToken);
310
325
  const ws = new WebSocket(u.toString());
311
326
  this.resumeSocket = ws;
@@ -341,6 +356,7 @@ var Publisher = class {
341
356
  }
342
357
  terminateWithError(err) {
343
358
  this.stopped = true;
359
+ this.cancelMediaRecovery();
344
360
  this.reconnectGeneration++;
345
361
  this.resumeSocket?.close();
346
362
  this.resumeSocket = null;
@@ -348,12 +364,13 @@ var Publisher = class {
348
364
  this.sig = null;
349
365
  this.pc?.close();
350
366
  this.pc = null;
367
+ this.unwatchStreamTracks();
351
368
  this.localStream?.getTracks().forEach((track) => track.stop());
352
369
  this.localStream = null;
353
370
  this.hasAnswer = false;
354
371
  this.pendingRemoteCandidates = [];
355
372
  this.readToken = null;
356
- this.selectedGatewayURL = null;
373
+ this.gatewayURL = null;
357
374
  this.opts.callbacks?.onError?.(err);
358
375
  }
359
376
  handleSignal(msg) {
@@ -395,6 +412,15 @@ var Publisher = class {
395
412
  case "connection_state": {
396
413
  break;
397
414
  }
415
+ case "media_stall":
416
+ case "media_track_ended": {
417
+ void this.beginMediaRecovery(msg.track);
418
+ break;
419
+ }
420
+ case "media_resumed": {
421
+ this.completeMediaRecovery(msg.track);
422
+ break;
423
+ }
398
424
  case "error": {
399
425
  this.opts.callbacks?.onError?.(new Error(msg.error));
400
426
  break;
@@ -403,6 +429,107 @@ var Publisher = class {
403
429
  break;
404
430
  }
405
431
  }
432
+ async beginMediaRecovery(trackType) {
433
+ if (this.stopped || this.recoveringMedia || this.recoveryRequired) return;
434
+ const tracks = this.localStream?.getVideoTracks() ?? [];
435
+ const liveTracks = tracks.filter((track) => track.readyState !== "ended");
436
+ if (liveTracks.length === 0) {
437
+ this.failMediaRecovery(trackType, "capture_ended");
438
+ return;
439
+ }
440
+ this.recoveringMedia = true;
441
+ const generation = ++this.recoveryGeneration;
442
+ this.recoveryAction = "sender_restart";
443
+ this.emitRecoveryTransition({ state: "recovering", track: trackType, action: "sender_restart" });
444
+ this.sendRecoveryDiagnostic("recovery_started", trackType, "sender_restart");
445
+ await this.restartSenders(liveTracks, generation);
446
+ if (!this.isCurrentRecovery(generation)) return;
447
+ await this.wait(senderRecoveryWaitMs);
448
+ if (!this.isCurrentRecovery(generation)) return;
449
+ this.recoveryAction = "ice_restart";
450
+ this.emitRecoveryTransition({ state: "recovering", track: trackType, action: "ice_restart" });
451
+ this.sendRecoveryDiagnostic("recovery_retry", trackType, "ice_restart");
452
+ try {
453
+ await this.renegotiate(true);
454
+ } catch {
455
+ }
456
+ await this.wait(iceRecoveryWaitMs);
457
+ if (!this.isCurrentRecovery(generation)) return;
458
+ this.failMediaRecovery(trackType, "automatic_recovery_failed");
459
+ }
460
+ async restartSenders(tracks, generation) {
461
+ const live = new Set(tracks);
462
+ const senders = this.pc?.getSenders().filter(
463
+ (sender) => sender.track && live.has(sender.track)
464
+ ) ?? [];
465
+ if (senders.length === 0) return;
466
+ const originals = senders.map((sender) => ({ sender, track: sender.track }));
467
+ try {
468
+ await Promise.all(originals.map(({ sender }) => sender.replaceTrack(null)));
469
+ await this.wait(senderRestartPauseMs);
470
+ if (!this.isCurrentRecovery(generation)) return;
471
+ await Promise.all(originals.map(({ sender, track }) => sender.replaceTrack(track)));
472
+ await this.renegotiate(false);
473
+ } catch {
474
+ }
475
+ }
476
+ async renegotiate(iceRestart) {
477
+ const pc = this.pc;
478
+ const signaling = this.sig;
479
+ if (!pc || !signaling) throw new Error("publisher signaling is unavailable");
480
+ if (iceRestart) pc.restartIce?.();
481
+ const offer = await pc.createOffer(iceRestart ? { iceRestart: true } : void 0);
482
+ await pc.setLocalDescription(offer);
483
+ await this.gatherComplete();
484
+ const local = pc.localDescription;
485
+ if (!local) throw new Error("local description missing");
486
+ this.hasAnswer = false;
487
+ this.pendingRemoteCandidates = [];
488
+ signaling.send({ type: "offer", sdp: local.sdp, sdp_type: "offer" });
489
+ }
490
+ completeMediaRecovery(trackType) {
491
+ if (!this.recoveringMedia) return;
492
+ const action = this.recoveryAction ?? void 0;
493
+ this.cancelMediaRecovery();
494
+ this.emitRecoveryTransition({ state: "recovered", track: trackType, action });
495
+ }
496
+ failMediaRecovery(trackType, reason) {
497
+ if (this.stopped || this.recoveryRequired) return;
498
+ this.recoveryRequired = true;
499
+ const action = this.recoveryAction ?? void 0;
500
+ this.cancelMediaRecovery();
501
+ const event = { state: "failed", track: trackType, action, reason };
502
+ this.emitRecoveryTransition(event);
503
+ this.opts.callbacks?.onRecoveryRequired?.(event);
504
+ this.sendRecoveryDiagnostic("recovery_failed", trackType, action, reason);
505
+ }
506
+ cancelMediaRecovery() {
507
+ this.recoveryGeneration++;
508
+ this.recoveringMedia = false;
509
+ this.recoveryAction = null;
510
+ }
511
+ isCurrentRecovery(generation) {
512
+ return !this.stopped && this.recoveringMedia && generation === this.recoveryGeneration;
513
+ }
514
+ emitRecoveryTransition(event) {
515
+ this.opts.callbacks?.onRecoveryStateChange?.(event);
516
+ }
517
+ sendRecoveryDiagnostic(event, track, action, reason) {
518
+ this.sig?.send({ type: "recovery_event", event, track, action, reason });
519
+ }
520
+ watchStreamTracks(stream) {
521
+ for (const track of stream.getVideoTracks()) {
522
+ const handler = () => this.failMediaRecovery("screen", "capture_ended");
523
+ track.addEventListener("ended", handler);
524
+ this.trackEndHandlers.set(track, handler);
525
+ }
526
+ }
527
+ unwatchStreamTracks() {
528
+ for (const [track, handler] of this.trackEndHandlers) {
529
+ track.removeEventListener("ended", handler);
530
+ }
531
+ this.trackEndHandlers.clear();
532
+ }
406
533
  /** Waits for ICE gathering to reach the "complete" state. */
407
534
  gatherComplete() {
408
535
  return new Promise((resolve) => {
@@ -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\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":[]}
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 PublisherRecoveryEvent,\n PublisherRecoveryState,\n PublisherRecoveryAction,\n PublisherRecoveryFailureReason,\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 {\n GatewayReadyInfo,\n PublisherOptions,\n PublisherRecoveryAction,\n PublisherRecoveryEvent,\n SignalMessage,\n} from \"./types\";\n\nconst defaultSignalingReconnectTimeoutMs = 20_000;\nconst signalingResumeAttemptTimeoutMs = 3_000;\nconst signalingResumeMaxBackoffMs = 3_000;\nconst senderRestartPauseMs = 100;\nconst senderRecoveryWaitMs = 4_000;\nconst iceRecoveryWaitMs = 8_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 gatewayURL: string | null = null;\n private stopped = true;\n private reconnecting = false;\n private reconnectGeneration = 0;\n private resumeSocket: WebSocket | null = null;\n private recoveryGeneration = 0;\n private recoveringMedia = false;\n private recoveryRequired = false;\n private recoveryAction: PublisherRecoveryAction | null = null;\n private trackEndHandlers = new Map<MediaStreamTrack, EventListener>();\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 * The signaling gateway URL that won the initial race, or null before start.\n * Relay this with frameReadToken so the application server can reach the same\n * region for frame reads and change-notification subscriptions.\n */\n get selectedGatewayURL(): string | null { return this.gatewayURL; }\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.recoveryRequired = false;\n this.localStream = stream;\n this.watchStreamTracks(stream);\n\n // Race all gateways; returns winning WebSocket + TURN/read-token info\n const { ws, readyInfo, gatewayURL } = await this.raceGateways();\n this.gatewayURL = 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 this.cancelMediaRecovery();\n this.recoveryRequired = false;\n this.unwatchStreamTracks();\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 this.watchStreamTracks(stream);\n\n await this.renegotiate(false);\n }\n\n /** Stops publishing and tears down the peer connection. */\n stop(): void {\n this.stopped = true;\n this.cancelMediaRecovery();\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.unwatchStreamTracks();\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.gatewayURL = 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.gatewayURL || !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.gatewayURL!);\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.cancelMediaRecovery();\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.unwatchStreamTracks();\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.gatewayURL = 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 \"media_stall\":\n case \"media_track_ended\": {\n void this.beginMediaRecovery(msg.track);\n break;\n }\n\n case \"media_resumed\": {\n this.completeMediaRecovery(msg.track);\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 private async beginMediaRecovery(trackType: string): Promise<void> {\n if (this.stopped || this.recoveringMedia || this.recoveryRequired) return;\n\n const tracks = this.localStream?.getVideoTracks() ?? [];\n const liveTracks = tracks.filter((track) => track.readyState !== \"ended\");\n if (liveTracks.length === 0) {\n this.failMediaRecovery(trackType, \"capture_ended\");\n return;\n }\n\n this.recoveringMedia = true;\n const generation = ++this.recoveryGeneration;\n this.recoveryAction = \"sender_restart\";\n this.emitRecoveryTransition({ state: \"recovering\", track: trackType, action: \"sender_restart\" });\n this.sendRecoveryDiagnostic(\"recovery_started\", trackType, \"sender_restart\");\n\n await this.restartSenders(liveTracks, generation);\n if (!this.isCurrentRecovery(generation)) return;\n\n await this.wait(senderRecoveryWaitMs);\n if (!this.isCurrentRecovery(generation)) return;\n\n this.recoveryAction = \"ice_restart\";\n this.emitRecoveryTransition({ state: \"recovering\", track: trackType, action: \"ice_restart\" });\n this.sendRecoveryDiagnostic(\"recovery_retry\", trackType, \"ice_restart\");\n try {\n await this.renegotiate(true);\n } catch {\n // The fixed recovery window below still gives signaling-resume a chance.\n // If media does not recover, the host receives onRecoveryRequired.\n }\n\n await this.wait(iceRecoveryWaitMs);\n if (!this.isCurrentRecovery(generation)) return;\n this.failMediaRecovery(trackType, \"automatic_recovery_failed\");\n }\n\n private async restartSenders(tracks: MediaStreamTrack[], generation: number): Promise<void> {\n const live = new Set(tracks);\n const senders = this.pc?.getSenders().filter(\n (sender) => sender.track && live.has(sender.track),\n ) ?? [];\n if (senders.length === 0) return;\n\n const originals = senders.map((sender) => ({ sender, track: sender.track! }));\n try {\n await Promise.all(originals.map(({ sender }) => sender.replaceTrack(null)));\n await this.wait(senderRestartPauseMs);\n if (!this.isCurrentRecovery(generation)) return;\n await Promise.all(originals.map(({ sender, track }) => sender.replaceTrack(track)));\n await this.renegotiate(false);\n } catch {\n // ICE restart is the second stage and may still recover a sender reset or\n // renegotiation failure, so do not fail the stream at this stage.\n }\n }\n\n private async renegotiate(iceRestart: boolean): Promise<void> {\n const pc = this.pc;\n const signaling = this.sig;\n if (!pc || !signaling) throw new Error(\"publisher signaling is unavailable\");\n\n if (iceRestart) pc.restartIce?.();\n const offer = await pc.createOffer(iceRestart ? { iceRestart: true } : undefined);\n await pc.setLocalDescription(offer);\n await this.gatherComplete();\n\n const local = pc.localDescription;\n if (!local) throw new Error(\"local description missing\");\n this.hasAnswer = false;\n this.pendingRemoteCandidates = [];\n signaling.send({ type: \"offer\", sdp: local.sdp, sdp_type: \"offer\" });\n }\n\n private completeMediaRecovery(trackType: string): void {\n if (!this.recoveringMedia) return;\n const action = this.recoveryAction ?? undefined;\n this.cancelMediaRecovery();\n this.emitRecoveryTransition({ state: \"recovered\", track: trackType, action });\n }\n\n private failMediaRecovery(\n trackType: string,\n reason: \"capture_ended\" | \"automatic_recovery_failed\",\n ): void {\n if (this.stopped || this.recoveryRequired) return;\n this.recoveryRequired = true;\n const action = this.recoveryAction ?? undefined;\n this.cancelMediaRecovery();\n const event: PublisherRecoveryEvent = { state: \"failed\", track: trackType, action, reason };\n this.emitRecoveryTransition(event);\n this.opts.callbacks?.onRecoveryRequired?.(event);\n this.sendRecoveryDiagnostic(\"recovery_failed\", trackType, action, reason);\n }\n\n private cancelMediaRecovery(): void {\n this.recoveryGeneration++;\n this.recoveringMedia = false;\n this.recoveryAction = null;\n }\n\n private isCurrentRecovery(generation: number): boolean {\n return !this.stopped && this.recoveringMedia && generation === this.recoveryGeneration;\n }\n\n private emitRecoveryTransition(event: PublisherRecoveryEvent): void {\n this.opts.callbacks?.onRecoveryStateChange?.(event);\n }\n\n private sendRecoveryDiagnostic(\n event: \"recovery_started\" | \"recovery_retry\" | \"recovery_failed\",\n track: string,\n action?: PublisherRecoveryAction,\n reason?: \"capture_ended\" | \"automatic_recovery_failed\",\n ): void {\n this.sig?.send({ type: \"recovery_event\", event, track, action, reason });\n }\n\n private watchStreamTracks(stream: MediaStream): void {\n for (const track of stream.getVideoTracks()) {\n const handler: EventListener = () => this.failMediaRecovery(\"screen\", \"capture_ended\");\n track.addEventListener(\"ended\", handler);\n this.trackEndHandlers.set(track, handler);\n }\n }\n\n private unwatchStreamTracks(): void {\n for (const [track, handler] of this.trackEndHandlers) {\n track.removeEventListener(\"ended\", handler);\n }\n this.trackEndHandlers.clear();\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
@@ -18,6 +18,24 @@ type SignalMessage = {
18
18
  } | {
19
19
  type: "connection_state";
20
20
  state: string;
21
+ } | {
22
+ type: "media_stall";
23
+ track: string;
24
+ frame_age_ms: number;
25
+ } | {
26
+ type: "media_resumed";
27
+ track: string;
28
+ duration_ms: number;
29
+ } | {
30
+ type: "media_track_ended";
31
+ track: string;
32
+ reason?: string;
33
+ } | {
34
+ type: "recovery_event";
35
+ event: "recovery_started" | "recovery_retry" | "recovery_failed";
36
+ track: string;
37
+ action?: "sender_restart" | "ice_restart";
38
+ reason?: "capture_ended" | "automatic_recovery_failed";
21
39
  } | {
22
40
  type: "error";
23
41
  error: string;
@@ -44,6 +62,20 @@ interface PublisherCallbacks {
44
62
  onError?: (error: Error) => void;
45
63
  /** Called when the browser has successfully connected to the media server. */
46
64
  onConnected?: () => void;
65
+ /** Called for each transition in automatic media recovery. */
66
+ onRecoveryStateChange?: (event: PublisherRecoveryEvent) => void;
67
+ /** Called when recovery requires the host application to obtain a new screen share. */
68
+ onRecoveryRequired?: (event: PublisherRecoveryEvent) => void;
69
+ }
70
+ type PublisherRecoveryState = "recovering" | "recovered" | "failed";
71
+ type PublisherRecoveryAction = "sender_restart" | "ice_restart";
72
+ type PublisherRecoveryFailureReason = "capture_ended" | "automatic_recovery_failed";
73
+ /** A transition in the publisher's fixed automatic media-recovery ladder. */
74
+ interface PublisherRecoveryEvent {
75
+ state: PublisherRecoveryState;
76
+ track: string;
77
+ action?: PublisherRecoveryAction;
78
+ reason?: PublisherRecoveryFailureReason;
47
79
  }
48
80
  /**
49
81
  * TURN and read-token info delivered in the gateway `ready` message.
@@ -102,14 +134,25 @@ declare class Publisher {
102
134
  private pendingRemoteCandidates;
103
135
  private localStream;
104
136
  private readToken;
105
- private selectedGatewayURL;
137
+ private gatewayURL;
106
138
  private stopped;
107
139
  private reconnecting;
108
140
  private reconnectGeneration;
109
141
  private resumeSocket;
142
+ private recoveryGeneration;
143
+ private recoveringMedia;
144
+ private recoveryRequired;
145
+ private recoveryAction;
146
+ private trackEndHandlers;
110
147
  constructor(opts: PublisherOptions);
111
148
  /** The read token used for frame fetches and signaling resume in the selected region. */
112
149
  get frameReadToken(): string | null;
150
+ /**
151
+ * The signaling gateway URL that won the initial race, or null before start.
152
+ * Relay this with frameReadToken so the application server can reach the same
153
+ * region for frame reads and change-notification subscriptions.
154
+ */
155
+ get selectedGatewayURL(): string | null;
113
156
  /**
114
157
  * Starts the publisher: races all gateways to find the fastest, completes
115
158
  * the two-phase handshake, creates the peer connection, and sends the SDP
@@ -132,6 +175,17 @@ declare class Publisher {
132
175
  private wait;
133
176
  private terminateWithError;
134
177
  private handleSignal;
178
+ private beginMediaRecovery;
179
+ private restartSenders;
180
+ private renegotiate;
181
+ private completeMediaRecovery;
182
+ private failMediaRecovery;
183
+ private cancelMediaRecovery;
184
+ private isCurrentRecovery;
185
+ private emitRecoveryTransition;
186
+ private sendRecoveryDiagnostic;
187
+ private watchStreamTracks;
188
+ private unwatchStreamTracks;
135
189
  /** Waits for ICE gathering to reach the "complete" state. */
136
190
  private gatherComplete;
137
191
  }
@@ -253,4 +307,4 @@ declare function captureCamera(opts?: CaptureCameraOptions): Promise<MediaStream
253
307
  */
254
308
  declare function captureScreen(opts?: CaptureScreenOptions): Promise<MediaStream>;
255
309
 
256
- export { type CaptureCameraOptions, type CaptureScreenOptions, type GatewayReadyInfo, Publisher, type PublisherCallbacks, type PublisherOptions, type SignalMessage, captureCamera, captureScreen };
310
+ export { type CaptureCameraOptions, type CaptureScreenOptions, type GatewayReadyInfo, Publisher, type PublisherCallbacks, type PublisherOptions, type PublisherRecoveryAction, type PublisherRecoveryEvent, type PublisherRecoveryFailureReason, type PublisherRecoveryState, type SignalMessage, captureCamera, captureScreen };
package/dist/index.d.ts CHANGED
@@ -18,6 +18,24 @@ type SignalMessage = {
18
18
  } | {
19
19
  type: "connection_state";
20
20
  state: string;
21
+ } | {
22
+ type: "media_stall";
23
+ track: string;
24
+ frame_age_ms: number;
25
+ } | {
26
+ type: "media_resumed";
27
+ track: string;
28
+ duration_ms: number;
29
+ } | {
30
+ type: "media_track_ended";
31
+ track: string;
32
+ reason?: string;
33
+ } | {
34
+ type: "recovery_event";
35
+ event: "recovery_started" | "recovery_retry" | "recovery_failed";
36
+ track: string;
37
+ action?: "sender_restart" | "ice_restart";
38
+ reason?: "capture_ended" | "automatic_recovery_failed";
21
39
  } | {
22
40
  type: "error";
23
41
  error: string;
@@ -44,6 +62,20 @@ interface PublisherCallbacks {
44
62
  onError?: (error: Error) => void;
45
63
  /** Called when the browser has successfully connected to the media server. */
46
64
  onConnected?: () => void;
65
+ /** Called for each transition in automatic media recovery. */
66
+ onRecoveryStateChange?: (event: PublisherRecoveryEvent) => void;
67
+ /** Called when recovery requires the host application to obtain a new screen share. */
68
+ onRecoveryRequired?: (event: PublisherRecoveryEvent) => void;
69
+ }
70
+ type PublisherRecoveryState = "recovering" | "recovered" | "failed";
71
+ type PublisherRecoveryAction = "sender_restart" | "ice_restart";
72
+ type PublisherRecoveryFailureReason = "capture_ended" | "automatic_recovery_failed";
73
+ /** A transition in the publisher's fixed automatic media-recovery ladder. */
74
+ interface PublisherRecoveryEvent {
75
+ state: PublisherRecoveryState;
76
+ track: string;
77
+ action?: PublisherRecoveryAction;
78
+ reason?: PublisherRecoveryFailureReason;
47
79
  }
48
80
  /**
49
81
  * TURN and read-token info delivered in the gateway `ready` message.
@@ -102,14 +134,25 @@ declare class Publisher {
102
134
  private pendingRemoteCandidates;
103
135
  private localStream;
104
136
  private readToken;
105
- private selectedGatewayURL;
137
+ private gatewayURL;
106
138
  private stopped;
107
139
  private reconnecting;
108
140
  private reconnectGeneration;
109
141
  private resumeSocket;
142
+ private recoveryGeneration;
143
+ private recoveringMedia;
144
+ private recoveryRequired;
145
+ private recoveryAction;
146
+ private trackEndHandlers;
110
147
  constructor(opts: PublisherOptions);
111
148
  /** The read token used for frame fetches and signaling resume in the selected region. */
112
149
  get frameReadToken(): string | null;
150
+ /**
151
+ * The signaling gateway URL that won the initial race, or null before start.
152
+ * Relay this with frameReadToken so the application server can reach the same
153
+ * region for frame reads and change-notification subscriptions.
154
+ */
155
+ get selectedGatewayURL(): string | null;
113
156
  /**
114
157
  * Starts the publisher: races all gateways to find the fastest, completes
115
158
  * the two-phase handshake, creates the peer connection, and sends the SDP
@@ -132,6 +175,17 @@ declare class Publisher {
132
175
  private wait;
133
176
  private terminateWithError;
134
177
  private handleSignal;
178
+ private beginMediaRecovery;
179
+ private restartSenders;
180
+ private renegotiate;
181
+ private completeMediaRecovery;
182
+ private failMediaRecovery;
183
+ private cancelMediaRecovery;
184
+ private isCurrentRecovery;
185
+ private emitRecoveryTransition;
186
+ private sendRecoveryDiagnostic;
187
+ private watchStreamTracks;
188
+ private unwatchStreamTracks;
135
189
  /** Waits for ICE gathering to reach the "complete" state. */
136
190
  private gatherComplete;
137
191
  }
@@ -253,4 +307,4 @@ declare function captureCamera(opts?: CaptureCameraOptions): Promise<MediaStream
253
307
  */
254
308
  declare function captureScreen(opts?: CaptureScreenOptions): Promise<MediaStream>;
255
309
 
256
- export { type CaptureCameraOptions, type CaptureScreenOptions, type GatewayReadyInfo, Publisher, type PublisherCallbacks, type PublisherOptions, type SignalMessage, captureCamera, captureScreen };
310
+ export { type CaptureCameraOptions, type CaptureScreenOptions, type GatewayReadyInfo, Publisher, type PublisherCallbacks, type PublisherOptions, type PublisherRecoveryAction, type PublisherRecoveryEvent, type PublisherRecoveryFailureReason, type PublisherRecoveryState, type SignalMessage, captureCamera, captureScreen };
package/dist/index.js CHANGED
@@ -48,6 +48,9 @@ function parseSignal(data) {
48
48
  var defaultSignalingReconnectTimeoutMs = 2e4;
49
49
  var signalingResumeAttemptTimeoutMs = 3e3;
50
50
  var signalingResumeMaxBackoffMs = 3e3;
51
+ var senderRestartPauseMs = 100;
52
+ var senderRecoveryWaitMs = 4e3;
53
+ var iceRecoveryWaitMs = 8e3;
51
54
  var Publisher = class {
52
55
  opts;
53
56
  sig = null;
@@ -56,11 +59,16 @@ var Publisher = class {
56
59
  pendingRemoteCandidates = [];
57
60
  localStream = null;
58
61
  readToken = null;
59
- selectedGatewayURL = null;
62
+ gatewayURL = null;
60
63
  stopped = true;
61
64
  reconnecting = false;
62
65
  reconnectGeneration = 0;
63
66
  resumeSocket = null;
67
+ recoveryGeneration = 0;
68
+ recoveringMedia = false;
69
+ recoveryRequired = false;
70
+ recoveryAction = null;
71
+ trackEndHandlers = /* @__PURE__ */ new Map();
64
72
  constructor(opts) {
65
73
  this.opts = opts;
66
74
  }
@@ -68,6 +76,14 @@ var Publisher = class {
68
76
  get frameReadToken() {
69
77
  return this.readToken;
70
78
  }
79
+ /**
80
+ * The signaling gateway URL that won the initial race, or null before start.
81
+ * Relay this with frameReadToken so the application server can reach the same
82
+ * region for frame reads and change-notification subscriptions.
83
+ */
84
+ get selectedGatewayURL() {
85
+ return this.gatewayURL;
86
+ }
71
87
  /**
72
88
  * Starts the publisher: races all gateways to find the fastest, completes
73
89
  * the two-phase handshake, creates the peer connection, and sends the SDP
@@ -76,9 +92,11 @@ var Publisher = class {
76
92
  */
77
93
  async start(stream) {
78
94
  this.stopped = false;
95
+ this.recoveryRequired = false;
79
96
  this.localStream = stream;
97
+ this.watchStreamTracks(stream);
80
98
  const { ws, readyInfo, gatewayURL } = await this.raceGateways();
81
- this.selectedGatewayURL = gatewayURL;
99
+ this.gatewayURL = gatewayURL;
82
100
  if (readyInfo.read_token) {
83
101
  this.readToken = readyInfo.read_token;
84
102
  }
@@ -121,6 +139,9 @@ var Publisher = class {
121
139
  /** Replaces the currently published stream with a new one. */
122
140
  async replaceStream(stream) {
123
141
  if (!this.pc) throw new Error("publisher not started");
142
+ this.cancelMediaRecovery();
143
+ this.recoveryRequired = false;
144
+ this.unwatchStreamTracks();
124
145
  const senders = this.pc.getSenders();
125
146
  for (const sender of senders) {
126
147
  if (sender.track) {
@@ -131,26 +152,20 @@ var Publisher = class {
131
152
  this.pc.addTrack(track, stream);
132
153
  }
133
154
  this.localStream = stream;
134
- const offer = await this.pc.createOffer();
135
- await this.pc.setLocalDescription(offer);
136
- await this.gatherComplete();
137
- const local = this.pc.localDescription;
138
- if (!local) throw new Error("local description missing");
139
- this.sig?.send({
140
- type: "offer",
141
- sdp: local.sdp,
142
- sdp_type: "offer"
143
- });
155
+ this.watchStreamTracks(stream);
156
+ await this.renegotiate(false);
144
157
  }
145
158
  /** Stops publishing and tears down the peer connection. */
146
159
  stop() {
147
160
  this.stopped = true;
161
+ this.cancelMediaRecovery();
148
162
  this.reconnectGeneration++;
149
163
  this.reconnecting = false;
150
164
  this.resumeSocket?.close();
151
165
  this.resumeSocket = null;
152
166
  this.sig?.close();
153
167
  this.sig = null;
168
+ this.unwatchStreamTracks();
154
169
  this.localStream?.getTracks().forEach((t) => t.stop());
155
170
  this.localStream = null;
156
171
  this.pc?.close();
@@ -158,7 +173,7 @@ var Publisher = class {
158
173
  this.hasAnswer = false;
159
174
  this.pendingRemoteCandidates = [];
160
175
  this.readToken = null;
161
- this.selectedGatewayURL = null;
176
+ this.gatewayURL = null;
162
177
  }
163
178
  /** Returns the current RTCPeerConnection, or null if not started. */
164
179
  get peerConnection() {
@@ -238,7 +253,7 @@ var Publisher = class {
238
253
  }
239
254
  async resumeSignaling() {
240
255
  if (this.reconnecting || this.stopped) return;
241
- if (!this.selectedGatewayURL || !this.readToken) {
256
+ if (!this.gatewayURL || !this.readToken) {
242
257
  this.terminateWithError(new Error("signaling closed and cannot be resumed"));
243
258
  return;
244
259
  }
@@ -277,7 +292,7 @@ var Publisher = class {
277
292
  }
278
293
  openResumeSocket(timeoutMs) {
279
294
  return new Promise((resolve, reject) => {
280
- const u = new URL(this.selectedGatewayURL);
295
+ const u = new URL(this.gatewayURL);
281
296
  u.searchParams.set("token", this.readToken);
282
297
  const ws = new WebSocket(u.toString());
283
298
  this.resumeSocket = ws;
@@ -313,6 +328,7 @@ var Publisher = class {
313
328
  }
314
329
  terminateWithError(err) {
315
330
  this.stopped = true;
331
+ this.cancelMediaRecovery();
316
332
  this.reconnectGeneration++;
317
333
  this.resumeSocket?.close();
318
334
  this.resumeSocket = null;
@@ -320,12 +336,13 @@ var Publisher = class {
320
336
  this.sig = null;
321
337
  this.pc?.close();
322
338
  this.pc = null;
339
+ this.unwatchStreamTracks();
323
340
  this.localStream?.getTracks().forEach((track) => track.stop());
324
341
  this.localStream = null;
325
342
  this.hasAnswer = false;
326
343
  this.pendingRemoteCandidates = [];
327
344
  this.readToken = null;
328
- this.selectedGatewayURL = null;
345
+ this.gatewayURL = null;
329
346
  this.opts.callbacks?.onError?.(err);
330
347
  }
331
348
  handleSignal(msg) {
@@ -367,6 +384,15 @@ var Publisher = class {
367
384
  case "connection_state": {
368
385
  break;
369
386
  }
387
+ case "media_stall":
388
+ case "media_track_ended": {
389
+ void this.beginMediaRecovery(msg.track);
390
+ break;
391
+ }
392
+ case "media_resumed": {
393
+ this.completeMediaRecovery(msg.track);
394
+ break;
395
+ }
370
396
  case "error": {
371
397
  this.opts.callbacks?.onError?.(new Error(msg.error));
372
398
  break;
@@ -375,6 +401,107 @@ var Publisher = class {
375
401
  break;
376
402
  }
377
403
  }
404
+ async beginMediaRecovery(trackType) {
405
+ if (this.stopped || this.recoveringMedia || this.recoveryRequired) return;
406
+ const tracks = this.localStream?.getVideoTracks() ?? [];
407
+ const liveTracks = tracks.filter((track) => track.readyState !== "ended");
408
+ if (liveTracks.length === 0) {
409
+ this.failMediaRecovery(trackType, "capture_ended");
410
+ return;
411
+ }
412
+ this.recoveringMedia = true;
413
+ const generation = ++this.recoveryGeneration;
414
+ this.recoveryAction = "sender_restart";
415
+ this.emitRecoveryTransition({ state: "recovering", track: trackType, action: "sender_restart" });
416
+ this.sendRecoveryDiagnostic("recovery_started", trackType, "sender_restart");
417
+ await this.restartSenders(liveTracks, generation);
418
+ if (!this.isCurrentRecovery(generation)) return;
419
+ await this.wait(senderRecoveryWaitMs);
420
+ if (!this.isCurrentRecovery(generation)) return;
421
+ this.recoveryAction = "ice_restart";
422
+ this.emitRecoveryTransition({ state: "recovering", track: trackType, action: "ice_restart" });
423
+ this.sendRecoveryDiagnostic("recovery_retry", trackType, "ice_restart");
424
+ try {
425
+ await this.renegotiate(true);
426
+ } catch {
427
+ }
428
+ await this.wait(iceRecoveryWaitMs);
429
+ if (!this.isCurrentRecovery(generation)) return;
430
+ this.failMediaRecovery(trackType, "automatic_recovery_failed");
431
+ }
432
+ async restartSenders(tracks, generation) {
433
+ const live = new Set(tracks);
434
+ const senders = this.pc?.getSenders().filter(
435
+ (sender) => sender.track && live.has(sender.track)
436
+ ) ?? [];
437
+ if (senders.length === 0) return;
438
+ const originals = senders.map((sender) => ({ sender, track: sender.track }));
439
+ try {
440
+ await Promise.all(originals.map(({ sender }) => sender.replaceTrack(null)));
441
+ await this.wait(senderRestartPauseMs);
442
+ if (!this.isCurrentRecovery(generation)) return;
443
+ await Promise.all(originals.map(({ sender, track }) => sender.replaceTrack(track)));
444
+ await this.renegotiate(false);
445
+ } catch {
446
+ }
447
+ }
448
+ async renegotiate(iceRestart) {
449
+ const pc = this.pc;
450
+ const signaling = this.sig;
451
+ if (!pc || !signaling) throw new Error("publisher signaling is unavailable");
452
+ if (iceRestart) pc.restartIce?.();
453
+ const offer = await pc.createOffer(iceRestart ? { iceRestart: true } : void 0);
454
+ await pc.setLocalDescription(offer);
455
+ await this.gatherComplete();
456
+ const local = pc.localDescription;
457
+ if (!local) throw new Error("local description missing");
458
+ this.hasAnswer = false;
459
+ this.pendingRemoteCandidates = [];
460
+ signaling.send({ type: "offer", sdp: local.sdp, sdp_type: "offer" });
461
+ }
462
+ completeMediaRecovery(trackType) {
463
+ if (!this.recoveringMedia) return;
464
+ const action = this.recoveryAction ?? void 0;
465
+ this.cancelMediaRecovery();
466
+ this.emitRecoveryTransition({ state: "recovered", track: trackType, action });
467
+ }
468
+ failMediaRecovery(trackType, reason) {
469
+ if (this.stopped || this.recoveryRequired) return;
470
+ this.recoveryRequired = true;
471
+ const action = this.recoveryAction ?? void 0;
472
+ this.cancelMediaRecovery();
473
+ const event = { state: "failed", track: trackType, action, reason };
474
+ this.emitRecoveryTransition(event);
475
+ this.opts.callbacks?.onRecoveryRequired?.(event);
476
+ this.sendRecoveryDiagnostic("recovery_failed", trackType, action, reason);
477
+ }
478
+ cancelMediaRecovery() {
479
+ this.recoveryGeneration++;
480
+ this.recoveringMedia = false;
481
+ this.recoveryAction = null;
482
+ }
483
+ isCurrentRecovery(generation) {
484
+ return !this.stopped && this.recoveringMedia && generation === this.recoveryGeneration;
485
+ }
486
+ emitRecoveryTransition(event) {
487
+ this.opts.callbacks?.onRecoveryStateChange?.(event);
488
+ }
489
+ sendRecoveryDiagnostic(event, track, action, reason) {
490
+ this.sig?.send({ type: "recovery_event", event, track, action, reason });
491
+ }
492
+ watchStreamTracks(stream) {
493
+ for (const track of stream.getVideoTracks()) {
494
+ const handler = () => this.failMediaRecovery("screen", "capture_ended");
495
+ track.addEventListener("ended", handler);
496
+ this.trackEndHandlers.set(track, handler);
497
+ }
498
+ }
499
+ unwatchStreamTracks() {
500
+ for (const [track, handler] of this.trackEndHandlers) {
501
+ track.removeEventListener("ended", handler);
502
+ }
503
+ this.trackEndHandlers.clear();
504
+ }
378
505
  /** Waits for ICE gathering to reach the "complete" state. */
379
506
  gatherComplete() {
380
507
  return new Promise((resolve) => {
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\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":[]}
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 {\n GatewayReadyInfo,\n PublisherOptions,\n PublisherRecoveryAction,\n PublisherRecoveryEvent,\n SignalMessage,\n} from \"./types\";\n\nconst defaultSignalingReconnectTimeoutMs = 20_000;\nconst signalingResumeAttemptTimeoutMs = 3_000;\nconst signalingResumeMaxBackoffMs = 3_000;\nconst senderRestartPauseMs = 100;\nconst senderRecoveryWaitMs = 4_000;\nconst iceRecoveryWaitMs = 8_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 gatewayURL: string | null = null;\n private stopped = true;\n private reconnecting = false;\n private reconnectGeneration = 0;\n private resumeSocket: WebSocket | null = null;\n private recoveryGeneration = 0;\n private recoveringMedia = false;\n private recoveryRequired = false;\n private recoveryAction: PublisherRecoveryAction | null = null;\n private trackEndHandlers = new Map<MediaStreamTrack, EventListener>();\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 * The signaling gateway URL that won the initial race, or null before start.\n * Relay this with frameReadToken so the application server can reach the same\n * region for frame reads and change-notification subscriptions.\n */\n get selectedGatewayURL(): string | null { return this.gatewayURL; }\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.recoveryRequired = false;\n this.localStream = stream;\n this.watchStreamTracks(stream);\n\n // Race all gateways; returns winning WebSocket + TURN/read-token info\n const { ws, readyInfo, gatewayURL } = await this.raceGateways();\n this.gatewayURL = 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 this.cancelMediaRecovery();\n this.recoveryRequired = false;\n this.unwatchStreamTracks();\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 this.watchStreamTracks(stream);\n\n await this.renegotiate(false);\n }\n\n /** Stops publishing and tears down the peer connection. */\n stop(): void {\n this.stopped = true;\n this.cancelMediaRecovery();\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.unwatchStreamTracks();\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.gatewayURL = 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.gatewayURL || !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.gatewayURL!);\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.cancelMediaRecovery();\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.unwatchStreamTracks();\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.gatewayURL = 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 \"media_stall\":\n case \"media_track_ended\": {\n void this.beginMediaRecovery(msg.track);\n break;\n }\n\n case \"media_resumed\": {\n this.completeMediaRecovery(msg.track);\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 private async beginMediaRecovery(trackType: string): Promise<void> {\n if (this.stopped || this.recoveringMedia || this.recoveryRequired) return;\n\n const tracks = this.localStream?.getVideoTracks() ?? [];\n const liveTracks = tracks.filter((track) => track.readyState !== \"ended\");\n if (liveTracks.length === 0) {\n this.failMediaRecovery(trackType, \"capture_ended\");\n return;\n }\n\n this.recoveringMedia = true;\n const generation = ++this.recoveryGeneration;\n this.recoveryAction = \"sender_restart\";\n this.emitRecoveryTransition({ state: \"recovering\", track: trackType, action: \"sender_restart\" });\n this.sendRecoveryDiagnostic(\"recovery_started\", trackType, \"sender_restart\");\n\n await this.restartSenders(liveTracks, generation);\n if (!this.isCurrentRecovery(generation)) return;\n\n await this.wait(senderRecoveryWaitMs);\n if (!this.isCurrentRecovery(generation)) return;\n\n this.recoveryAction = \"ice_restart\";\n this.emitRecoveryTransition({ state: \"recovering\", track: trackType, action: \"ice_restart\" });\n this.sendRecoveryDiagnostic(\"recovery_retry\", trackType, \"ice_restart\");\n try {\n await this.renegotiate(true);\n } catch {\n // The fixed recovery window below still gives signaling-resume a chance.\n // If media does not recover, the host receives onRecoveryRequired.\n }\n\n await this.wait(iceRecoveryWaitMs);\n if (!this.isCurrentRecovery(generation)) return;\n this.failMediaRecovery(trackType, \"automatic_recovery_failed\");\n }\n\n private async restartSenders(tracks: MediaStreamTrack[], generation: number): Promise<void> {\n const live = new Set(tracks);\n const senders = this.pc?.getSenders().filter(\n (sender) => sender.track && live.has(sender.track),\n ) ?? [];\n if (senders.length === 0) return;\n\n const originals = senders.map((sender) => ({ sender, track: sender.track! }));\n try {\n await Promise.all(originals.map(({ sender }) => sender.replaceTrack(null)));\n await this.wait(senderRestartPauseMs);\n if (!this.isCurrentRecovery(generation)) return;\n await Promise.all(originals.map(({ sender, track }) => sender.replaceTrack(track)));\n await this.renegotiate(false);\n } catch {\n // ICE restart is the second stage and may still recover a sender reset or\n // renegotiation failure, so do not fail the stream at this stage.\n }\n }\n\n private async renegotiate(iceRestart: boolean): Promise<void> {\n const pc = this.pc;\n const signaling = this.sig;\n if (!pc || !signaling) throw new Error(\"publisher signaling is unavailable\");\n\n if (iceRestart) pc.restartIce?.();\n const offer = await pc.createOffer(iceRestart ? { iceRestart: true } : undefined);\n await pc.setLocalDescription(offer);\n await this.gatherComplete();\n\n const local = pc.localDescription;\n if (!local) throw new Error(\"local description missing\");\n this.hasAnswer = false;\n this.pendingRemoteCandidates = [];\n signaling.send({ type: \"offer\", sdp: local.sdp, sdp_type: \"offer\" });\n }\n\n private completeMediaRecovery(trackType: string): void {\n if (!this.recoveringMedia) return;\n const action = this.recoveryAction ?? undefined;\n this.cancelMediaRecovery();\n this.emitRecoveryTransition({ state: \"recovered\", track: trackType, action });\n }\n\n private failMediaRecovery(\n trackType: string,\n reason: \"capture_ended\" | \"automatic_recovery_failed\",\n ): void {\n if (this.stopped || this.recoveryRequired) return;\n this.recoveryRequired = true;\n const action = this.recoveryAction ?? undefined;\n this.cancelMediaRecovery();\n const event: PublisherRecoveryEvent = { state: \"failed\", track: trackType, action, reason };\n this.emitRecoveryTransition(event);\n this.opts.callbacks?.onRecoveryRequired?.(event);\n this.sendRecoveryDiagnostic(\"recovery_failed\", trackType, action, reason);\n }\n\n private cancelMediaRecovery(): void {\n this.recoveryGeneration++;\n this.recoveringMedia = false;\n this.recoveryAction = null;\n }\n\n private isCurrentRecovery(generation: number): boolean {\n return !this.stopped && this.recoveringMedia && generation === this.recoveryGeneration;\n }\n\n private emitRecoveryTransition(event: PublisherRecoveryEvent): void {\n this.opts.callbacks?.onRecoveryStateChange?.(event);\n }\n\n private sendRecoveryDiagnostic(\n event: \"recovery_started\" | \"recovery_retry\" | \"recovery_failed\",\n track: string,\n action?: PublisherRecoveryAction,\n reason?: \"capture_ended\" | \"automatic_recovery_failed\",\n ): void {\n this.sig?.send({ type: \"recovery_event\", event, track, action, reason });\n }\n\n private watchStreamTracks(stream: MediaStream): void {\n for (const track of stream.getVideoTracks()) {\n const handler: EventListener = () => this.failMediaRecovery(\"screen\", \"capture_ended\");\n track.addEventListener(\"ended\", handler);\n this.trackEndHandlers.set(track, handler);\n }\n }\n\n private unwatchStreamTracks(): void {\n for (const [track, handler] of this.trackEndHandlers) {\n track.removeEventListener(\"ended\", handler);\n }\n this.trackEndHandlers.clear();\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":";AAaO,IAAM,mBAAN,MAAM,kBAAiB;AAAA,EACpB;AAAA;AAAA,EAGR,YAAmD;AAAA;AAAA,EAEnD,UAA+B;AAAA;AAAA,EAE/B,UAAyC;AAAA,EAEjC,YAAY,IAAe;AACjC,SAAK,KAAK;AAAA,EACZ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,OAAO,KAAK,IAAiC;AAC3C,UAAM,KAAK,IAAI,kBAAiB,EAAE;AAClC,OAAG,YAAY,CAAC,OAAqB;AACnC,YAAM,MAAM,YAAY,GAAG,IAAI;AAC/B,UAAI,IAAK,IAAG,YAAY,GAAG;AAAA,IAC7B;AACA,OAAG,UAAU,MAAM,GAAG,UAAU,IAAI,MAAM,iBAAiB,CAAC;AAC5D,OAAG,UAAU,MAAM,GAAG,UAAU;AAChC,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,KAAK,KAA0B;AAC7B,QAAI,KAAK,GAAG,eAAe,UAAU,MAAM;AACzC,WAAK,GAAG,KAAK,KAAK,UAAU,GAAG,CAAC;AAAA,IAClC;AAAA,EACF;AAAA;AAAA,EAGA,QAAc;AACZ,SAAK,GAAG,MAAM;AAAA,EAChB;AACF;AAGA,SAAS,YAAY,MAAqC;AACxD,MAAI;AACF,WAAO,KAAK,MAAM,IAAc;AAAA,EAClC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;;;ACtDA,IAAM,qCAAqC;AAC3C,IAAM,kCAAkC;AACxC,IAAM,8BAA8B;AACpC,IAAM,uBAAuB;AAC7B,IAAM,uBAAuB;AAC7B,IAAM,oBAAoB;AAwBnB,IAAM,YAAN,MAAgB;AAAA,EACb;AAAA,EACA,MAA+B;AAAA,EAC/B,KAA+B;AAAA,EAC/B,YAAY;AAAA,EACZ,0BAAiD,CAAC;AAAA,EAClD,cAAkC;AAAA,EAClC,YAA2B;AAAA,EAC3B,aAA4B;AAAA,EAC5B,UAAU;AAAA,EACV,eAAe;AAAA,EACf,sBAAsB;AAAA,EACtB,eAAiC;AAAA,EACjC,qBAAqB;AAAA,EACrB,kBAAkB;AAAA,EAClB,mBAAmB;AAAA,EACnB,iBAAiD;AAAA,EACjD,mBAAmB,oBAAI,IAAqC;AAAA,EAEpE,YAAY,MAAwB;AAClC,SAAK,OAAO;AAAA,EACd;AAAA;AAAA,EAGA,IAAI,iBAAgC;AAAE,WAAO,KAAK;AAAA,EAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAO7D,IAAI,qBAAoC;AAAE,WAAO,KAAK;AAAA,EAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQlE,MAAM,MAAM,QAAoC;AAC9C,SAAK,UAAU;AACf,SAAK,mBAAmB;AACxB,SAAK,cAAc;AACnB,SAAK,kBAAkB,MAAM;AAG7B,UAAM,EAAE,IAAI,WAAW,WAAW,IAAI,MAAM,KAAK,aAAa;AAC9D,SAAK,aAAa;AAGlB,QAAI,UAAU,YAAY;AACxB,WAAK,YAAY,UAAU;AAAA,IAC7B;AAMA,UAAM,aAA6B,CAAC,GAAI,KAAK,KAAK,cAAc,CAAC,CAAE;AACnE,QAAI,UAAU,aAAa,UAAU,UAAU,SAAS,GAAG;AACzD,iBAAW,KAAK;AAAA,QACd,MAAM,UAAU;AAAA,QAChB,UAAU,UAAU;AAAA,QACpB,YAAY,UAAU;AAAA,MACxB,CAAC;AAAA,IACH;AAEA,SAAK,KAAK,IAAI,kBAAkB,EAAE,WAAW,CAAC;AAE9C,SAAK,GAAG,iBAAiB,CAAC,OAAO;AAC/B,UAAI,CAAC,GAAG,aAAa,CAAC,KAAK,IAAK;AAChC,YAAM,IAAI,GAAG;AACb,WAAK,IAAI,KAAK;AAAA,QACZ,MAAM;AAAA,QACN,WAAW,EAAE;AAAA,QACb,SAAS,EAAE,UAAU;AAAA,QACrB,iBAAiB,EAAE,iBAAiB;AAAA,QACpC,mBAAmB,EAAE,oBAAoB;AAAA,MAC3C,CAAC;AAAA,IACH;AAEA,SAAK,GAAG,0BAA0B,MAAM;AACtC,YAAM,QAAQ,KAAK,IAAI;AACvB,UAAI,MAAO,MAAK,KAAK,WAAW,0BAA0B,KAAK;AAC/D,UAAI,UAAU,YAAa,MAAK,KAAK,WAAW,cAAc;AAAA,IAChE;AAEA,eAAW,SAAS,OAAO,UAAU,GAAG;AACtC,WAAK,GAAG,SAAS,OAAO,MAAM;AAAA,IAChC;AAEA,UAAM,QAAQ,MAAM,KAAK,GAAG,YAAY;AACxC,UAAM,KAAK,GAAG,oBAAoB,KAAK;AACvC,UAAM,KAAK,eAAe;AAG1B,UAAM,YAAY,KAAK,iBAAiB,EAAE;AAE1C,UAAM,QAAQ,KAAK,GAAG;AACtB,QAAI,CAAC,MAAO,OAAM,IAAI,MAAM,wCAAwC;AACpE,cAAU,KAAK,EAAE,MAAM,SAAS,KAAK,MAAM,KAAK,UAAU,QAAQ,CAAC;AAAA,EACrE;AAAA;AAAA,EAGA,MAAM,cAAc,QAAoC;AACtD,QAAI,CAAC,KAAK,GAAI,OAAM,IAAI,MAAM,uBAAuB;AAErD,SAAK,oBAAoB;AACzB,SAAK,mBAAmB;AACxB,SAAK,oBAAoB;AAGzB,UAAM,UAAU,KAAK,GAAG,WAAW;AACnC,eAAW,UAAU,SAAS;AAC5B,UAAI,OAAO,OAAO;AAChB,aAAK,GAAG,YAAY,MAAM;AAAA,MAC5B;AAAA,IACF;AAGA,eAAW,SAAS,OAAO,UAAU,GAAG;AACtC,WAAK,GAAG,SAAS,OAAO,MAAM;AAAA,IAChC;AACA,SAAK,cAAc;AACnB,SAAK,kBAAkB,MAAM;AAE7B,UAAM,KAAK,YAAY,KAAK;AAAA,EAC9B;AAAA;AAAA,EAGA,OAAa;AACX,SAAK,UAAU;AACf,SAAK,oBAAoB;AACzB,SAAK;AACL,SAAK,eAAe;AACpB,SAAK,cAAc,MAAM;AACzB,SAAK,eAAe;AACpB,SAAK,KAAK,MAAM;AAChB,SAAK,MAAM;AAEX,SAAK,oBAAoB;AACzB,SAAK,aAAa,UAAU,EAAE,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC;AACrD,SAAK,cAAc;AAEnB,SAAK,IAAI,MAAM;AACf,SAAK,KAAK;AACV,SAAK,YAAY;AACjB,SAAK,0BAA0B,CAAC;AAChC,SAAK,YAAY;AACjB,SAAK,aAAa;AAAA,EACpB;AAAA;AAAA,EAGA,IAAI,iBAA2C;AAC7C,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,IAAI,cAAuB;AACzB,WAAO,KAAK,IAAI,oBAAoB;AAAA,EACtC;AAAA;AAAA;AAAA;AAAA,EAMQ,eAA4F;AAClG,WAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,YAAM,EAAE,aAAa,MAAM,IAAI,KAAK;AACpC,UAAI,YAAY,WAAW,GAAG;AAC5B,eAAO,IAAI,MAAM,0BAA0B,CAAC;AAC5C;AAAA,MACF;AAEA,YAAM,UAAuB,CAAC;AAC9B,UAAI,UAAU;AAEd,YAAM,WAAW,CAAC,WAAuB;AACvC,mBAAW,KAAK,SAAS;AACvB,cAAI,MAAM,QAAQ;AAChB,cAAE,YAAY;AACd,cAAE,UAAU;AACZ,cAAE,UAAU;AACZ,cAAE,MAAM;AAAA,UACV;AAAA,QACF;AAAA,MACF;AAEA,YAAM,iBAAiB,MAAM;AAC3B,YAAI,QAAS;AACb,YAAI,QAAQ,MAAM,OAAK,EAAE,eAAe,UAAU,UAAU,EAAE,eAAe,UAAU,OAAO,GAAG;AAC/F,oBAAU;AACV,iBAAO,IAAI,MAAM,gCAAgC,CAAC;AAAA,QACpD;AAAA,MACF;AAEA,iBAAW,cAAc,aAAa;AACpC,cAAM,IAAI,IAAI,IAAI,UAAU;AAC5B,UAAE,aAAa,IAAI,SAAS,KAAK;AACjC,cAAM,KAAK,IAAI,UAAU,EAAE,SAAS,CAAC;AACrC,gBAAQ,KAAK,EAAE;AAEf,YAAI,WAAW;AAEf,WAAG,YAAY,CAAC,OAAqB;AACnC,cAAI,QAAS;AACb,cAAI;AACF,kBAAM,MAAM,KAAK,MAAM,GAAG,IAAc;AACxC,gBAAI,CAAC,YAAY,IAAI,SAAS,YAAY;AACxC,yBAAW;AACX,iBAAG,KAAK,KAAK,UAAU,EAAE,MAAM,UAAU,CAAC,CAAC;AAAA,YAC7C,WAAW,YAAY,IAAI,SAAS,SAAS;AAC3C,wBAAU;AACV,uBAAS,EAAE;AACX,sBAAQ,EAAE,IAAI,WAAW,KAAyB,WAAW,CAAC;AAAA,YAChE;AAAA,UACF,QAAQ;AAAA,UAER;AAAA,QACF;AAEA,WAAG,UAAU,MAAM,eAAe;AAClC,WAAG,UAAU,MAAM,eAAe;AAAA,MACpC;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEQ,iBAAiB,IAAiC;AACxD,UAAM,UAAU,iBAAiB,KAAK,EAAE;AACxC,SAAK,MAAM;AACX,YAAQ,YAAY,CAAC,QAAQ,KAAK,aAAa,GAAG;AAClD,YAAQ,UAAU,MAAM;AACtB,UAAI,KAAK,QAAQ,WAAW,KAAK,QAAS;AAC1C,WAAK,MAAM;AACX,WAAK,KAAK,gBAAgB;AAAA,IAC5B;AAGA,YAAQ,UAAU,MAAM;AAAA,IAAC;AACzB,WAAO;AAAA,EACT;AAAA,EAEA,MAAc,kBAAiC;AAC7C,QAAI,KAAK,gBAAgB,KAAK,QAAS;AACvC,QAAI,CAAC,KAAK,cAAc,CAAC,KAAK,WAAW;AACvC,WAAK,mBAAmB,IAAI,MAAM,wCAAwC,CAAC;AAC3E;AAAA,IACF;AAEA,SAAK,eAAe;AACpB,UAAM,aAAa,EAAE,KAAK;AAC1B,UAAM,UAAU,KAAK,KAAK,+BAA+B;AACzD,UAAM,WAAW,KAAK,IAAI,IAAI,KAAK,IAAI,GAAG,OAAO;AACjD,QAAI,YAAY;AAEhB,WAAO,CAAC,KAAK,WAAW,eAAe,KAAK,uBAAuB,KAAK,IAAI,KAAK,UAAU;AACzF,UAAI,YAAY,GAAG;AACjB,cAAM,SAAS,KAAK,IAAI,WAAW,KAAK,IAAI,GAAG,WAAW,KAAK,IAAI,CAAC,CAAC;AACrE,YAAI,WAAW,EAAG;AAClB,cAAM,KAAK,KAAK,MAAM;AACtB,YAAI,KAAK,WAAW,eAAe,KAAK,oBAAqB;AAAA,MAC/D;AAEA,YAAM,YAAY,WAAW,KAAK,IAAI;AACtC,UAAI,YAAY,EAAG;AACnB,UAAI;AACF,cAAM,KAAK,MAAM,KAAK,iBAAiB,KAAK,IAAI,iCAAiC,KAAK,IAAI,GAAG,SAAS,CAAC,CAAC;AACxG,YAAI,KAAK,WAAW,eAAe,KAAK,qBAAqB;AAC3D,aAAG,MAAM;AACT;AAAA,QACF;AACA,aAAK,eAAe;AACpB,aAAK,eAAe;AACpB,aAAK,iBAAiB,EAAE;AACxB;AAAA,MACF,QAAQ;AACN,oBAAY,cAAc,IAAI,MAAM,KAAK,IAAI,YAAY,GAAG,2BAA2B;AAAA,MACzF;AAAA,IACF;AAEA,QAAI,CAAC,KAAK,WAAW,eAAe,KAAK,qBAAqB;AAC5D,WAAK,eAAe;AACpB,WAAK,mBAAmB,IAAI,MAAM,sDAAsD,CAAC;AAAA,IAC3F;AAAA,EACF;AAAA,EAEQ,iBAAiB,WAAuC;AAC9D,WAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,YAAM,IAAI,IAAI,IAAI,KAAK,UAAW;AAClC,QAAE,aAAa,IAAI,SAAS,KAAK,SAAU;AAC3C,YAAM,KAAK,IAAI,UAAU,EAAE,SAAS,CAAC;AACrC,WAAK,eAAe;AACpB,UAAI,UAAU;AACd,YAAM,QAAQ,WAAW,MAAM,KAAK,GAAG,SAAS;AAEhD,YAAM,OAAO,MAAM;AACjB,YAAI,QAAS;AACb,kBAAU;AACV,qBAAa,KAAK;AAClB,YAAI,KAAK,iBAAiB,GAAI,MAAK,eAAe;AAClD,WAAG,YAAY;AACf,WAAG,UAAU;AACb,WAAG,UAAU;AACb,WAAG,MAAM;AACT,eAAO,IAAI,MAAM,iCAAiC,CAAC;AAAA,MACrD;AAEA,SAAG,YAAY,CAAC,OAAqB;AACnC,YAAI;AACF,gBAAM,MAAM,KAAK,MAAM,GAAG,IAAc;AACxC,cAAI,IAAI,SAAS,aAAa,QAAS;AACvC,oBAAU;AACV,uBAAa,KAAK;AAClB,kBAAQ,EAAE;AAAA,QACZ,QAAQ;AAAA,QAER;AAAA,MACF;AACA,SAAG,UAAU;AACb,SAAG,UAAU;AAAA,IACf,CAAC;AAAA,EACH;AAAA,EAEQ,KAAK,IAA2B;AACtC,WAAO,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,EAAE,CAAC;AAAA,EACzD;AAAA,EAEQ,mBAAmB,KAAkB;AAC3C,SAAK,UAAU;AACf,SAAK,oBAAoB;AACzB,SAAK;AACL,SAAK,cAAc,MAAM;AACzB,SAAK,eAAe;AACpB,SAAK,KAAK,MAAM;AAChB,SAAK,MAAM;AACX,SAAK,IAAI,MAAM;AACf,SAAK,KAAK;AACV,SAAK,oBAAoB;AACzB,SAAK,aAAa,UAAU,EAAE,QAAQ,CAAC,UAAU,MAAM,KAAK,CAAC;AAC7D,SAAK,cAAc;AACnB,SAAK,YAAY;AACjB,SAAK,0BAA0B,CAAC;AAChC,SAAK,YAAY;AACjB,SAAK,aAAa;AAClB,SAAK,KAAK,WAAW,UAAU,GAAG;AAAA,EACpC;AAAA,EAEQ,aAAa,KAA0B;AAC7C,YAAQ,IAAI,MAAM;AAAA,MAChB,KAAK,UAAU;AACb,YAAI,CAAC,KAAK,GAAI;AACd,aAAK,GACF;AAAA,UACC,IAAI,sBAAsB,EAAE,MAAM,UAAU,KAAK,IAAI,IAAI,CAAC;AAAA,QAC5D,EACC,KAAK,MAAM;AACV,eAAK,YAAY;AAEjB,qBAAW,QAAQ,KAAK,yBAAyB;AAC/C,iBAAK,IAAI,gBAAgB,IAAI,EAAE,MAAM,MAAM;AAAA,YAE3C,CAAC;AAAA,UACH;AACA,eAAK,0BAA0B,CAAC;AAAA,QAClC,CAAC,EACA,MAAM,CAAC,QAAQ;AACd,eAAK,KAAK,WAAW;AAAA,YACnB,IAAI,MAAM,qCAAqC,GAAG,EAAE;AAAA,UACtD;AAAA,QACF,CAAC;AACH;AAAA,MACF;AAAA,MAEA,KAAK,iBAAiB;AACpB,YAAI,CAAC,KAAK,GAAI;AACd,cAAM,OAA4B;AAAA,UAChC,WAAW,IAAI;AAAA,UACf,QAAQ,IAAI,WAAW;AAAA,UACvB,eAAe,IAAI,mBAAmB;AAAA,UACtC,kBAAkB,IAAI,qBAAqB;AAAA,QAC7C;AACA,YAAI,KAAK,WAAW;AAClB,eAAK,GAAG,gBAAgB,IAAI,EAAE,MAAM,MAAM;AAAA,UAE1C,CAAC;AAAA,QACH,OAAO;AACL,eAAK,wBAAwB,KAAK,IAAI;AAAA,QACxC;AACA;AAAA,MACF;AAAA,MAEA,KAAK,oBAAoB;AAIvB;AAAA,MACF;AAAA,MAEA,KAAK;AAAA,MACL,KAAK,qBAAqB;AACxB,aAAK,KAAK,mBAAmB,IAAI,KAAK;AACtC;AAAA,MACF;AAAA,MAEA,KAAK,iBAAiB;AACpB,aAAK,sBAAsB,IAAI,KAAK;AACpC;AAAA,MACF;AAAA,MAEA,KAAK,SAAS;AACZ,aAAK,KAAK,WAAW,UAAU,IAAI,MAAM,IAAI,KAAK,CAAC;AACnD;AAAA,MACF;AAAA,MAEA,KAAK;AACH;AAAA,IACJ;AAAA,EACF;AAAA,EAEA,MAAc,mBAAmB,WAAkC;AACjE,QAAI,KAAK,WAAW,KAAK,mBAAmB,KAAK,iBAAkB;AAEnE,UAAM,SAAS,KAAK,aAAa,eAAe,KAAK,CAAC;AACtD,UAAM,aAAa,OAAO,OAAO,CAAC,UAAU,MAAM,eAAe,OAAO;AACxE,QAAI,WAAW,WAAW,GAAG;AAC3B,WAAK,kBAAkB,WAAW,eAAe;AACjD;AAAA,IACF;AAEA,SAAK,kBAAkB;AACvB,UAAM,aAAa,EAAE,KAAK;AAC1B,SAAK,iBAAiB;AACtB,SAAK,uBAAuB,EAAE,OAAO,cAAc,OAAO,WAAW,QAAQ,iBAAiB,CAAC;AAC/F,SAAK,uBAAuB,oBAAoB,WAAW,gBAAgB;AAE3E,UAAM,KAAK,eAAe,YAAY,UAAU;AAChD,QAAI,CAAC,KAAK,kBAAkB,UAAU,EAAG;AAEzC,UAAM,KAAK,KAAK,oBAAoB;AACpC,QAAI,CAAC,KAAK,kBAAkB,UAAU,EAAG;AAEzC,SAAK,iBAAiB;AACtB,SAAK,uBAAuB,EAAE,OAAO,cAAc,OAAO,WAAW,QAAQ,cAAc,CAAC;AAC5F,SAAK,uBAAuB,kBAAkB,WAAW,aAAa;AACtE,QAAI;AACF,YAAM,KAAK,YAAY,IAAI;AAAA,IAC7B,QAAQ;AAAA,IAGR;AAEA,UAAM,KAAK,KAAK,iBAAiB;AACjC,QAAI,CAAC,KAAK,kBAAkB,UAAU,EAAG;AACzC,SAAK,kBAAkB,WAAW,2BAA2B;AAAA,EAC/D;AAAA,EAEA,MAAc,eAAe,QAA4B,YAAmC;AAC1F,UAAM,OAAO,IAAI,IAAI,MAAM;AAC3B,UAAM,UAAU,KAAK,IAAI,WAAW,EAAE;AAAA,MACpC,CAAC,WAAW,OAAO,SAAS,KAAK,IAAI,OAAO,KAAK;AAAA,IACnD,KAAK,CAAC;AACN,QAAI,QAAQ,WAAW,EAAG;AAE1B,UAAM,YAAY,QAAQ,IAAI,CAAC,YAAY,EAAE,QAAQ,OAAO,OAAO,MAAO,EAAE;AAC5E,QAAI;AACF,YAAM,QAAQ,IAAI,UAAU,IAAI,CAAC,EAAE,OAAO,MAAM,OAAO,aAAa,IAAI,CAAC,CAAC;AAC1E,YAAM,KAAK,KAAK,oBAAoB;AACpC,UAAI,CAAC,KAAK,kBAAkB,UAAU,EAAG;AACzC,YAAM,QAAQ,IAAI,UAAU,IAAI,CAAC,EAAE,QAAQ,MAAM,MAAM,OAAO,aAAa,KAAK,CAAC,CAAC;AAClF,YAAM,KAAK,YAAY,KAAK;AAAA,IAC9B,QAAQ;AAAA,IAGR;AAAA,EACF;AAAA,EAEA,MAAc,YAAY,YAAoC;AAC5D,UAAM,KAAK,KAAK;AAChB,UAAM,YAAY,KAAK;AACvB,QAAI,CAAC,MAAM,CAAC,UAAW,OAAM,IAAI,MAAM,oCAAoC;AAE3E,QAAI,WAAY,IAAG,aAAa;AAChC,UAAM,QAAQ,MAAM,GAAG,YAAY,aAAa,EAAE,YAAY,KAAK,IAAI,MAAS;AAChF,UAAM,GAAG,oBAAoB,KAAK;AAClC,UAAM,KAAK,eAAe;AAE1B,UAAM,QAAQ,GAAG;AACjB,QAAI,CAAC,MAAO,OAAM,IAAI,MAAM,2BAA2B;AACvD,SAAK,YAAY;AACjB,SAAK,0BAA0B,CAAC;AAChC,cAAU,KAAK,EAAE,MAAM,SAAS,KAAK,MAAM,KAAK,UAAU,QAAQ,CAAC;AAAA,EACrE;AAAA,EAEQ,sBAAsB,WAAyB;AACrD,QAAI,CAAC,KAAK,gBAAiB;AAC3B,UAAM,SAAS,KAAK,kBAAkB;AACtC,SAAK,oBAAoB;AACzB,SAAK,uBAAuB,EAAE,OAAO,aAAa,OAAO,WAAW,OAAO,CAAC;AAAA,EAC9E;AAAA,EAEQ,kBACN,WACA,QACM;AACN,QAAI,KAAK,WAAW,KAAK,iBAAkB;AAC3C,SAAK,mBAAmB;AACxB,UAAM,SAAS,KAAK,kBAAkB;AACtC,SAAK,oBAAoB;AACzB,UAAM,QAAgC,EAAE,OAAO,UAAU,OAAO,WAAW,QAAQ,OAAO;AAC1F,SAAK,uBAAuB,KAAK;AACjC,SAAK,KAAK,WAAW,qBAAqB,KAAK;AAC/C,SAAK,uBAAuB,mBAAmB,WAAW,QAAQ,MAAM;AAAA,EAC1E;AAAA,EAEQ,sBAA4B;AAClC,SAAK;AACL,SAAK,kBAAkB;AACvB,SAAK,iBAAiB;AAAA,EACxB;AAAA,EAEQ,kBAAkB,YAA6B;AACrD,WAAO,CAAC,KAAK,WAAW,KAAK,mBAAmB,eAAe,KAAK;AAAA,EACtE;AAAA,EAEQ,uBAAuB,OAAqC;AAClE,SAAK,KAAK,WAAW,wBAAwB,KAAK;AAAA,EACpD;AAAA,EAEQ,uBACN,OACA,OACA,QACA,QACM;AACN,SAAK,KAAK,KAAK,EAAE,MAAM,kBAAkB,OAAO,OAAO,QAAQ,OAAO,CAAC;AAAA,EACzE;AAAA,EAEQ,kBAAkB,QAA2B;AACnD,eAAW,SAAS,OAAO,eAAe,GAAG;AAC3C,YAAM,UAAyB,MAAM,KAAK,kBAAkB,UAAU,eAAe;AACrF,YAAM,iBAAiB,SAAS,OAAO;AACvC,WAAK,iBAAiB,IAAI,OAAO,OAAO;AAAA,IAC1C;AAAA,EACF;AAAA,EAEQ,sBAA4B;AAClC,eAAW,CAAC,OAAO,OAAO,KAAK,KAAK,kBAAkB;AACpD,YAAM,oBAAoB,SAAS,OAAO;AAAA,IAC5C;AACA,SAAK,iBAAiB,MAAM;AAAA,EAC9B;AAAA;AAAA,EAGQ,iBAAgC;AACtC,WAAO,IAAI,QAAQ,CAAC,YAAY;AAC9B,YAAM,KAAK,KAAK;AAChB,UAAI,CAAC,IAAI;AACP,gBAAQ;AACR;AAAA,MACF;AACA,UAAI,GAAG,sBAAsB,YAAY;AACvC,gBAAQ;AACR;AAAA,MACF;AACA,YAAM,UAAU,MAAM;AACpB,YAAI,GAAG,sBAAsB,YAAY;AACvC,aAAG,oBAAoB,2BAA2B,OAAO;AACzD,kBAAQ;AAAA,QACV;AAAA,MACF;AACA,SAAG,iBAAiB,2BAA2B,OAAO;AAAA,IACxD,CAAC;AAAA,EACH;AACF;;;AC9gBA,eAAsB,cACpB,OAA6B,CAAC,GACR;AACtB,QAAM,cAAsC;AAAA,IAC1C,OAAO,KAAK,SAAS;AAAA,MACnB,OAAO,EAAE,OAAO,KAAK;AAAA,MACrB,QAAQ,EAAE,OAAO,IAAI;AAAA,MACrB,WAAW,EAAE,OAAO,GAAG;AAAA,IACzB;AAAA,IACA,OAAO,KAAK,SAAS;AAAA,EACvB;AACA,QAAM,eAAe,KAAK,gBAAgB,UAAU;AACpD,SAAO,aAAa,aAAa,WAAW;AAC9C;AAiCA,eAAsB,cACpB,OAA6B,CAAC,GACR;AACtB,QAAM,cAAsC;AAAA,IAC1C,OAAO,KAAK,SAAS;AAAA,MACnB,OAAO,EAAE,KAAK,KAAK;AAAA,MACnB,WAAW,EAAE,OAAO,GAAG,KAAK,GAAG;AAAA,IACjC;AAAA,IACA,OAAO,KAAK,SAAS;AAAA,EACvB;AACA,QAAM,eAAe,KAAK,gBAAgB,UAAU;AACpD,SAAO,aAAa,gBAAgB,WAAW;AACjD;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@furious.luke/argus-js",
3
- "version": "0.3.0",
3
+ "version": "0.4.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",