@furious.luke/argus-js 0.3.1 → 0.5.0

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