@furious.luke/argus-js 0.4.0 → 0.5.1

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/dist/index.d.cts CHANGED
@@ -1,3 +1,39 @@
1
+ /**
2
+ * The logical purpose of a track within a stream. A stream may carry more than
3
+ * one track — camera and screen-share video, plus a single `audio` microphone
4
+ * track. Reads and change notifications address a specific video track by type;
5
+ * the `audio` track feeds server-side transcription rather than the frame path.
6
+ */
7
+ type TrackType = "camera" | "screen" | "audio";
8
+ /**
9
+ * The video track types. The video publish surface (start, publish, unpublish,
10
+ * replaceStream) is typed to this rather than {@link TrackType}, so `audio`
11
+ * cannot be passed to a video call — the microphone has its own dedicated API
12
+ * ({@link Publisher.startAudioOnly}, {@link Publisher.publishMicrophone}). A
13
+ * session may also begin without media via {@link Publisher.startTextOnly}.
14
+ */
15
+ type VideoTrackType = "camera" | "screen";
16
+ /**
17
+ * Declares the logical type and identity of a single published track.
18
+ *
19
+ * `mid` is the negotiated media-section id — the ROUTING key the server matches a
20
+ * track to its type by. It is used rather than the browser `MediaStreamTrack.id`
21
+ * because it is the only track identifier both peers agree on: Firefox mints an
22
+ * SDP msid track id unrelated to the `MediaStreamTrack.id`, so routing keyed on
23
+ * the latter cannot be matched server-side.
24
+ *
25
+ * `id` is the browser `MediaStreamTrack.id` — the GENERATION identity. The mid is
26
+ * stable across renegotiation and is reused when a track of the same type is
27
+ * republished on the same m-line, so it cannot distinguish a delayed end of a
28
+ * removed track from a failure of its replacement; the id changes with each new
29
+ * capture and can. The server echoes it in `media_track_ended`, and the publisher
30
+ * correlates that against the id it intentionally removed.
31
+ */
32
+ interface TrackLabel {
33
+ mid: string;
34
+ id: string;
35
+ type: TrackType;
36
+ }
1
37
  /**
2
38
  * Message types exchanged over the WebSocket signaling channel.
3
39
  */
@@ -5,10 +41,14 @@ type SignalMessage = {
5
41
  type: "offer";
6
42
  sdp: string;
7
43
  sdp_type: "offer";
44
+ tracks?: TrackLabel[];
45
+ negotiation_id?: string;
46
+ speech_enabled?: boolean;
8
47
  } | {
9
48
  type: "answer";
10
49
  sdp: string;
11
50
  sdp_type: "answer";
51
+ negotiation_id?: string;
12
52
  } | {
13
53
  type: "ice_candidate";
14
54
  candidate: string;
@@ -18,27 +58,38 @@ type SignalMessage = {
18
58
  } | {
19
59
  type: "connection_state";
20
60
  state: string;
61
+ } | {
62
+ type: "ice_path";
63
+ local_candidate_type: string;
64
+ local_protocol?: string;
65
+ remote_candidate_type?: string;
66
+ remote_protocol?: string;
67
+ relay_protocol?: string;
68
+ turn_url?: string;
21
69
  } | {
22
70
  type: "media_stall";
23
- track: string;
71
+ track: TrackType;
24
72
  frame_age_ms: number;
25
73
  } | {
26
74
  type: "media_resumed";
27
- track: string;
75
+ track: TrackType;
28
76
  duration_ms: number;
29
77
  } | {
30
78
  type: "media_track_ended";
31
- track: string;
79
+ track: TrackType;
80
+ track_id?: string;
32
81
  reason?: string;
33
82
  } | {
34
83
  type: "recovery_event";
35
84
  event: "recovery_started" | "recovery_retry" | "recovery_failed";
36
- track: string;
85
+ track: TrackType;
37
86
  action?: "sender_restart" | "ice_restart";
38
87
  reason?: "capture_ended" | "automatic_recovery_failed";
39
88
  } | {
40
89
  type: "error";
41
90
  error: string;
91
+ negotiation_id?: string;
92
+ fatal?: boolean;
42
93
  } | {
43
94
  type: "resumed";
44
95
  } | {
@@ -66,14 +117,31 @@ interface PublisherCallbacks {
66
117
  onRecoveryStateChange?: (event: PublisherRecoveryEvent) => void;
67
118
  /** Called when recovery requires the host application to obtain a new screen share. */
68
119
  onRecoveryRequired?: (event: PublisherRecoveryEvent) => void;
120
+ /** Called when paced assistant text arrives alongside synthesized speech. */
121
+ onAssistantText?: (event: AssistantTextEvent) => void;
122
+ /** Called when the server accepts or rejects typed user input. */
123
+ onUserTextResult?: (event: UserTextResultEvent) => void;
124
+ /** Called once the explicitly requested outbound speech track arrives. */
125
+ onSpeechTrack?: (track: MediaStreamTrack, streams: readonly MediaStream[]) => void;
126
+ }
127
+ interface AssistantTextEvent {
128
+ utteranceId: string;
129
+ text: string;
130
+ }
131
+ interface UserTextResultEvent {
132
+ messageId: string;
133
+ accepted: boolean;
134
+ reason?: string;
69
135
  }
70
136
  type PublisherRecoveryState = "recovering" | "recovered" | "failed";
71
137
  type PublisherRecoveryAction = "sender_restart" | "ice_restart";
72
138
  type PublisherRecoveryFailureReason = "capture_ended" | "automatic_recovery_failed";
139
+ /** Restricts gateway-advertised TURN URLs used for ICE candidate gathering. */
140
+ type TurnTransportPolicy = "all" | "udp" | "tls";
73
141
  /** A transition in the publisher's fixed automatic media-recovery ladder. */
74
142
  interface PublisherRecoveryEvent {
75
143
  state: PublisherRecoveryState;
76
- track: string;
144
+ track: TrackType;
77
145
  action?: PublisherRecoveryAction;
78
146
  reason?: PublisherRecoveryFailureReason;
79
147
  }
@@ -81,7 +149,7 @@ interface PublisherRecoveryEvent {
81
149
  * TURN and read-token info delivered in the gateway `ready` message.
82
150
  */
83
151
  interface GatewayReadyInfo {
84
- /** Ordered TURN URLs (UDP first, then TCP) so relay can fall back to TCP on UDP-blocking networks. */
152
+ /** Bounded TURN URLs with UDP and TCP transports for each selected relay server. */
85
153
  turn_urls?: string[];
86
154
  turn_username?: string;
87
155
  turn_credential?: string;
@@ -98,6 +166,29 @@ interface PublisherOptions {
98
166
  token: string;
99
167
  /** Optional extra ICE servers (e.g. STUN). TURN is supplied by the winning gateway. */
100
168
  iceServers?: RTCIceServer[];
169
+ /**
170
+ * ICE transport policy passed to the underlying `RTCPeerConnection`. Defaults
171
+ * to `"all"`, letting ICE pick the best path (usually direct). Set to
172
+ * `"relay"` to force media through the TURN relay only — no host or
173
+ * server-reflexive candidates — which is how you verify the TURN path end to
174
+ * end rather than have ICE silently bypass it.
175
+ */
176
+ iceTransportPolicy?: RTCIceTransportPolicy;
177
+ /**
178
+ * Restricts TURN URLs supplied by the winning gateway. Defaults to `"all"`.
179
+ * `"tls"` admits only secure `turns:` URLs and `"udp"` admits only TURN/UDP.
180
+ * Combine an explicit policy with `iceTransportPolicy: "relay"` when proving
181
+ * a particular relay path end to end rather than merely making it available.
182
+ */
183
+ turnTransportPolicy?: TurnTransportPolicy;
184
+ /**
185
+ * Overall deadline for the initial accepted/proceed/ready gateway race.
186
+ * Unaccepted sockets are replaced after three seconds within this deadline.
187
+ * Defaults to 20 seconds.
188
+ */
189
+ gatewayHandshakeTimeoutMs?: number;
190
+ /** Deadline after the initial offer for WebRTC to reach connected. Defaults to 30 seconds. */
191
+ peerConnectionTimeoutMs?: number;
101
192
  /** How long to retry a dropped signaling connection to the selected gateway. Defaults to 20 seconds. */
102
193
  signalingReconnectTimeoutMs?: number;
103
194
  /** Callbacks for lifecycle events. */
@@ -105,11 +196,12 @@ interface PublisherOptions {
105
196
  }
106
197
 
107
198
  /**
108
- * Publisher streams a browser {@link MediaStream} to an Argus media server over
109
- * WebRTC. Given the `gateway_urls` and `token` from a join-token response, it
199
+ * Publisher establishes a browser WebRTC session with an Argus media server.
200
+ * The session may start with camera, microphone, or only its reliable text data
201
+ * channel. Given the `gateway_urls` and `token` from a join-token response, it
110
202
  * races the candidate gateways to the fastest one, completes the two-phase
111
- * signaling handshake, and manages the peer connection — offer/answer exchange,
112
- * ICE candidate trickling, and track (re)negotiation.
203
+ * signaling handshake, and manages offer/answer exchange, ICE candidate
204
+ * trickling, and track (re)negotiation.
113
205
  *
114
206
  * After {@link Publisher.start} resolves, {@link Publisher.frameReadToken} holds
115
207
  * the token your application server needs to fetch frames for this stream.
@@ -132,18 +224,44 @@ declare class Publisher {
132
224
  private pc;
133
225
  private hasAnswer;
134
226
  private pendingRemoteCandidates;
135
- private localStream;
227
+ private pendingLocalCandidates;
228
+ private localCandidateOfferSent;
229
+ private retainedLocalCandidates;
230
+ private retainedLocalCandidateKeys;
231
+ private localCandidateGeneration;
232
+ private remoteCandidateKeys;
233
+ private remoteCandidateOrder;
234
+ private remoteCandidateGeneration;
136
235
  private readToken;
137
236
  private gatewayURL;
237
+ private lastReportedICEPath;
238
+ private watchedICETransports;
138
239
  private stopped;
240
+ private lifecycleGeneration;
241
+ private runAbort;
242
+ private peerConnectionTimer;
139
243
  private reconnecting;
140
244
  private reconnectGeneration;
141
245
  private resumeSocket;
142
- private recoveryGeneration;
143
- private recoveringMedia;
144
- private recoveryRequired;
145
- private recoveryAction;
246
+ private signalingWaiters;
247
+ private pendingOffer;
248
+ private recoverySequence;
249
+ private recoveryStates;
250
+ private iceRestartSequence;
251
+ private iceRestartAttempt;
146
252
  private trackEndHandlers;
253
+ private published;
254
+ private publishedStreams;
255
+ private typeSenders;
256
+ private intentionalTrackEnds;
257
+ private negotiationChain;
258
+ private pendingAnswer;
259
+ private negotiationSeq;
260
+ private textChannel;
261
+ private speechEnabled;
262
+ private speechPending;
263
+ private speechTransceiver;
264
+ private microphoneTransceiver;
147
265
  constructor(opts: PublisherOptions);
148
266
  /** The read token used for frame fetches and signaling resume in the selected region. */
149
267
  get frameReadToken(): string | null;
@@ -153,41 +271,218 @@ declare class Publisher {
153
271
  * region for frame reads and change-notification subscriptions.
154
272
  */
155
273
  get selectedGatewayURL(): string | null;
274
+ /** Requests the persistent outbound `speech` track. This is explicit user
275
+ * opt-in and renegotiates only once; the track remains silent between turns. */
276
+ enableSpeech(): Promise<void>;
277
+ /** Sends typed input over the reliable ordered Argus text channel. */
278
+ sendUserText(messageId: string, text: string): void;
156
279
  /**
157
280
  * Starts the publisher: races all gateways to find the fastest, completes
158
281
  * the two-phase handshake, creates the peer connection, and sends the SDP
159
282
  * offer. Resolves when the offer has been sent (not when ICE completes —
160
283
  * use onConnected for that).
284
+ *
285
+ * The stream's single video track is published under `type` (default `"camera"`),
286
+ * declared to the server so reads and change notifications can address them by
287
+ * type. Add or remove further tracks live with {@link Publisher.publish} and
288
+ * {@link Publisher.unpublish}.
289
+ */
290
+ start(stream: MediaStream, type?: VideoTrackType): Promise<void>;
291
+ /**
292
+ * Starts the publisher with a microphone track and no video — a fully valid
293
+ * audio-only stream, the natural starting point for a voice agent. Exactly one
294
+ * audio track must be present in `stream`. Video can be added later with
295
+ * {@link Publisher.publish}; a stream carries at most one microphone track.
296
+ *
297
+ * Like {@link Publisher.start} it races the gateways, completes the handshake,
298
+ * and sends the offer; it resolves once the offer is sent. The microphone is not
299
+ * subject to the video recovery ladder — a mic that stops simply ends
300
+ * transcription for the stream.
301
+ */
302
+ startAudioOnly(stream: MediaStream): Promise<void>;
303
+ /**
304
+ * Starts a WebRTC session with only the ordered `argus.text` data channel.
305
+ * This is the natural entry point for a typed, text-only agent: it requests no
306
+ * camera or microphone permission and publishes no media. Camera, screen, or
307
+ * microphone tracks can be added later with {@link Publisher.publish} or
308
+ * {@link Publisher.publishMicrophone}; {@link Publisher.enableSpeech} can add
309
+ * the optional inbound speech track independently.
310
+ */
311
+ startTextOnly(): Promise<void>;
312
+ /**
313
+ * Shared startup for video, audio-only, and text-only entry points: race the
314
+ * gateways, build the peer connection and text channel, optionally add an
315
+ * initial media track, and send the first offer.
316
+ */
317
+ private startSession;
318
+ /**
319
+ * Adds the single video track from `stream` to the live session under `type`,
320
+ * renegotiating so the media server begins ingesting them. Use this to add a
321
+ * track after {@link Publisher.start} — for example to begin a screen share on
322
+ * top of a live camera.
323
+ *
324
+ * Exactly one video track must be present in `stream`. If a track of `type` is already
325
+ * live it is removed and replaced (a "screen" published while another "screen"
326
+ * is live supersedes it).
327
+ */
328
+ publish(stream: MediaStream, type: VideoTrackType): Promise<void>;
329
+ /**
330
+ * Removes the live track(s) of the given type, stops their local capture, and
331
+ * renegotiates so the media server ends ingestion for that track. A no-op if
332
+ * no track of that type is published.
333
+ */
334
+ unpublish(type: VideoTrackType): Promise<void>;
335
+ /**
336
+ * Adds the microphone (audio) track from `stream` to the live session and
337
+ * renegotiates, so the media server begins transcribing it. Exactly one audio
338
+ * track must be present in `stream`. Publishing a microphone while one is
339
+ * already live replaces it.
340
+ *
341
+ * The audio track feeds server-side speech-to-text; its transcripts are
342
+ * delivered to the customer server over the change-notification subscription,
343
+ * not to the browser. Audio is not subject to the video recovery ladder — a
344
+ * mic that stops is simply removed.
345
+ */
346
+ publishMicrophone(stream: MediaStream): Promise<void>;
347
+ /**
348
+ * Removes the live microphone track, stops its local capture, and
349
+ * renegotiates so the media server ends transcription. A no-op if no
350
+ * microphone is published.
351
+ */
352
+ unpublishMicrophone(): Promise<void>;
353
+ /**
354
+ * Replaces the published track of a single type with a new stream and
355
+ * renegotiates in place — e.g. to swap to a freshly reacquired screen share
356
+ * after {@link PublisherCallbacks.onRecoveryRequired}. Defaults to the
357
+ * `"camera"` type. This is a convenience over {@link Publisher.publish}, which
358
+ * it delegates to (publishing one track per type replaces any existing track
359
+ * of that type).
161
360
  */
162
- start(stream: MediaStream): Promise<void>;
163
- /** Replaces the currently published stream with a new one. */
164
- replaceStream(stream: MediaStream): Promise<void>;
361
+ replaceStream(stream: MediaStream, type?: VideoTrackType): Promise<void>;
165
362
  /** Stops publishing and tears down the peer connection. */
166
363
  stop(): void;
364
+ private rejectPendingAnswer;
167
365
  /** Returns the current RTCPeerConnection, or null if not started. */
168
366
  get peerConnection(): RTCPeerConnection | null;
169
367
  /** Returns true if the peer connection is in the "connected" state. */
170
368
  get isConnected(): boolean;
171
369
  private raceGateways;
172
370
  private installSignaling;
371
+ private awaitSignaling;
372
+ private resolveSignalingWaiters;
373
+ private rejectSignalingWaiters;
374
+ private sendWhenSignalingAvailable;
375
+ private sendOffer;
173
376
  private resumeSignaling;
174
377
  private openResumeSocket;
175
378
  private wait;
379
+ private isActiveRun;
380
+ private assertActiveRun;
381
+ private armPeerConnectionTimeout;
382
+ private clearPeerConnectionTimeout;
176
383
  private terminateWithError;
177
384
  private handleSignal;
178
385
  private beginMediaRecovery;
179
386
  private restartSenders;
180
- private renegotiate;
387
+ /**
388
+ * Adds a recovery renegotiation to the same queue as user operations. The
389
+ * recovery ladder awaits its completion before starting the stage observation
390
+ * window. If recovery has completed by the time this reaches the head of the
391
+ * queue, it is skipped.
392
+ */
393
+ /** Returns the peer-wide ICE attempt shared by all active track recoveries. */
394
+ private sharedIceRestart;
395
+ /**
396
+ * Serializes a renegotiation onto the shared chain: it waits for any prior
397
+ * negotiation to finish (its answer applied), sends a fresh offer, and resolves
398
+ * only once this offer's answer has been applied. This prevents overlapping
399
+ * offers and answers being applied to the wrong offer.
400
+ */
401
+ private enqueueNegotiation;
402
+ private negotiateOnce;
403
+ /**
404
+ * Registers interest in the answer for the offer identified by `id` and returns
405
+ * a promise for its SDP. A second pending answer is an invariant violation: all
406
+ * offer creation, including recovery, must pass through negotiationChain.
407
+ */
408
+ private nextNegotiationId;
409
+ private handleTextMessage;
410
+ private awaitAnswer;
411
+ private negotiationAnswerTimeoutMs;
412
+ private handleLocalICECandidate;
413
+ private candidateKey;
414
+ private retainLocalCandidate;
415
+ private retainRemoteCandidate;
416
+ private clearRetainedICECandidates;
417
+ private beginLocalCandidateBatch;
418
+ private releaseLocalCandidateBatch;
419
+ private applyAnswered;
420
+ private watchSelectedICEPairChanges;
421
+ private reportSelectedICEPath;
181
422
  private completeMediaRecovery;
182
423
  private failMediaRecovery;
424
+ private recoveryState;
183
425
  private cancelMediaRecovery;
426
+ private cancelAllMediaRecovery;
184
427
  private isCurrentRecovery;
428
+ private hasActiveMediaRecovery;
429
+ private clearSharedIceRestartIfIdle;
185
430
  private emitRecoveryTransition;
186
431
  private sendRecoveryDiagnostic;
187
- private watchStreamTracks;
432
+ private requireSingleVideoTrack;
433
+ private requireLiveVideoTrack;
434
+ private requireLiveTrack;
435
+ private requireSingleAudioTrack;
436
+ /**
437
+ * Stages the microphone track onto the peer connection. Unlike video, audio
438
+ * has no recovery ladder and no SSIM/frame semantics, so this simply adds (or
439
+ * replaces) the single audio sender and declares the updated labels.
440
+ */
441
+ private stagePublishAudio;
442
+ /**
443
+ * Gives the microphone its own sendonly transceiver. addTrack() may reuse an
444
+ * existing compatible recvonly transceiver, which would collapse the
445
+ * microphone and assistant speech roles when speech was enabled first.
446
+ * addTransceiver() always creates a distinct m-line and its direction keeps
447
+ * the server's outbound speech sender on the dedicated speech transceiver.
448
+ */
449
+ private addMicrophoneTrack;
450
+ /** Registers one physical video track under its logical type and source stream. */
451
+ private registerTrack;
452
+ /** The live video tracks currently published under the given type. */
453
+ private tracksOfType;
454
+ /** Builds the id → type label array declared to the server on every offer. */
455
+ private buildTrackLabels;
456
+ private labelsReplacingType;
457
+ /**
458
+ * The negotiated mid of the transceiver currently sending `track`, or null if
459
+ * none is found or it has not been negotiated yet. The mid is the identifier
460
+ * both peers agree on; it is assigned once setLocalDescription runs, which the
461
+ * publisher always does before sending an offer's labels.
462
+ */
463
+ private midForTrack;
464
+ private stagePublish;
465
+ private stageUnpublish;
466
+ /** Stops every published local track and clears the published map. */
467
+ private stopPublishedTracks;
468
+ private expectIntentionalTrackEnd;
469
+ private forgetIntentionalTrackEnds;
470
+ private consumeIntentionalTrackEnd;
471
+ private clearIntentionalTrackEnds;
472
+ /**
473
+ * Watches a track's "ended" event so an involuntary capture stop (the user
474
+ * revokes a screen share, a device unplugs) reports as a recovery failure for
475
+ * that track's actual type. Intentional removals unwatch first.
476
+ */
477
+ private watchTrack;
478
+ /**
479
+ * Watches a microphone only for lifecycle removal. An ended microphone is not
480
+ * recovered like video; it is negotiated away so the media server can flush
481
+ * the utterance and release transcription resources.
482
+ */
483
+ private watchMicrophone;
484
+ private unwatchTrack;
188
485
  private unwatchStreamTracks;
189
- /** Waits for ICE gathering to reach the "complete" state. */
190
- private gatherComplete;
191
486
  }
192
487
 
193
488
  /**
@@ -306,5 +601,36 @@ declare function captureCamera(opts?: CaptureCameraOptions): Promise<MediaStream
306
601
  * ```
307
602
  */
308
603
  declare function captureScreen(opts?: CaptureScreenOptions): Promise<MediaStream>;
604
+ /**
605
+ * Options for {@link captureMicrophone}.
606
+ */
607
+ interface CaptureMicrophoneOptions {
608
+ /**
609
+ * Audio constraints, or `true`. Defaults to enabling the browser's echo
610
+ * cancellation, noise suppression, and auto gain control — the settings that
611
+ * give a speech-to-text engine the cleanest signal.
612
+ */
613
+ audio?: MediaTrackConstraints | boolean;
614
+ /**
615
+ * The `MediaDevices` instance to capture from. Defaults to the global
616
+ * `navigator.mediaDevices`.
617
+ */
618
+ mediaDevices?: MediaDevices;
619
+ }
620
+ /**
621
+ * Captures the user's microphone via `navigator.mediaDevices.getUserMedia`,
622
+ * applying defaults tuned for speech-to-text. Pair with
623
+ * {@link Publisher.publishMicrophone} to add transcription to a stream.
624
+ *
625
+ * Defaults:
626
+ * - `audio`: `{ echoCancellation: true, noiseSuppression: true, autoGainControl: true }`.
627
+ *
628
+ * @example
629
+ * ```ts
630
+ * const mic = await captureMicrophone();
631
+ * await publisher.publishMicrophone(mic);
632
+ * ```
633
+ */
634
+ declare function captureMicrophone(opts?: CaptureMicrophoneOptions): Promise<MediaStream>;
309
635
 
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 };
636
+ export { type AssistantTextEvent, type CaptureCameraOptions, type CaptureMicrophoneOptions, type CaptureScreenOptions, type GatewayReadyInfo, Publisher, type PublisherCallbacks, type PublisherOptions, type PublisherRecoveryAction, type PublisherRecoveryEvent, type PublisherRecoveryFailureReason, type PublisherRecoveryState, type SignalMessage, type TrackLabel, type TrackType, type TurnTransportPolicy, type UserTextResultEvent, type VideoTrackType, captureCamera, captureMicrophone, captureScreen };