@guava-ai/guava-sdk 0.39.0 → 0.41.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.
@@ -69,6 +69,7 @@ export class Call {
69
69
  private readonly _leavingTone?: Tone;
70
70
  private readonly _connectionTimeoutMs?: number;
71
71
  private readonly _customPayload: Record<string, unknown>;
72
+ private readonly _customAudioTrack?: MediaStreamTrack;
72
73
  private _connectionTimer?: ReturnType<typeof setTimeout>;
73
74
  private _sock?: WebSocket;
74
75
 
@@ -81,6 +82,7 @@ export class Call {
81
82
  private _pendingChatMessages: string[] = [];
82
83
 
83
84
  private _localStream?: MediaStream;
85
+ private _micMuted = false;
84
86
  private _peerConnection?: RTCPeerConnection;
85
87
  private _remoteStream?: MediaStream;
86
88
  private _iceServers?: RTCIceServer[];
@@ -94,6 +96,7 @@ export class Call {
94
96
  leavingTone = new LeavingTone(),
95
97
  connectionTimeoutMs = 30_000,
96
98
  customPayload = {},
99
+ audioTrack,
97
100
  }: {
98
101
  /** Code used to authenticate with the signaling server. */
99
102
  webrtcCode: string;
@@ -107,9 +110,18 @@ export class Call {
107
110
  connectionTimeoutMs?: number | "disabled";
108
111
  /** A custom payload that is forwarded to the agent at the start of the call. */
109
112
  customPayload?: Record<string, unknown>;
113
+ /**
114
+ * An audio track to send. Supplying this skips requesting the microphone with `getUserMedia()`.
115
+ * We use `track.clone()` to create a clone of this track. The caller is responsible for calling
116
+ * `track.stop()` on their original reference when it is no longer needed.
117
+ * Because we use a clone, use {@link Call.setMicrophoneMuted} to mute the call, rather than setting
118
+ * `enabled` on the original track.
119
+ */
120
+ audioTrack?: MediaStreamTrack;
110
121
  }) {
111
122
  this._webrtcCode = webrtcCode;
112
123
  this._customPayload = customPayload;
124
+ this._customAudioTrack = audioTrack;
113
125
  if (connectingTone !== "disabled") {
114
126
  this._connectingTone = connectingTone;
115
127
  }
@@ -212,29 +224,40 @@ export class Call {
212
224
  // Add the audio element.
213
225
  document.body.appendChild(this._remoteAudio);
214
226
 
215
- // We start by requesting device permissions first. The idea is that, after permission has been granted,
216
- // it makes sence to play a ringback, but it doesn't make sense to do so during the permission prompt.
227
+ if (this._customAudioTrack !== undefined) {
228
+ this._localStream = new MediaStream([this._customAudioTrack.clone()]);
229
+ } else {
230
+ // We start by requesting device permissions first. The idea is that, after permission has been granted,
231
+ // it makes sence to play a ringback, but it doesn't make sense to do so during the permission prompt.
217
232
 
218
- const permissionTimer = setTimeout(() => {
219
- this._setState("waiting-for-permission");
220
- }, 500);
233
+ const permissionTimer = setTimeout(() => {
234
+ this._setState("waiting-for-permission");
235
+ }, 500);
221
236
 
222
- try {
223
- this._localStream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
224
- } catch (error) {
225
- const callError = new CallError("MICROPHONE_ACCESS_FAILED", "Failed to access microphone.", {
226
- cause: error,
227
- });
228
- this._end(callError);
229
- throw callError;
230
- } finally {
231
- clearTimeout(permissionTimer);
232
- }
237
+ try {
238
+ this._localStream = await navigator.mediaDevices.getUserMedia({
239
+ audio: true,
240
+ video: false,
241
+ });
242
+ } catch (error) {
243
+ const callError = new CallError(
244
+ "MICROPHONE_ACCESS_FAILED",
245
+ "Failed to access microphone.",
246
+ {
247
+ cause: error,
248
+ },
249
+ );
250
+ this._end(callError);
251
+ throw callError;
252
+ } finally {
253
+ clearTimeout(permissionTimer);
254
+ }
233
255
 
234
- // hangup() was called during the permission prompt.
235
- if (this.state === "ended") {
236
- this._teardown();
237
- return;
256
+ // hangup() was called during the permission prompt.
257
+ if (this.state === "ended") {
258
+ this._teardown();
259
+ return;
260
+ }
238
261
  }
239
262
 
240
263
  // Now open and authenticate the signaling socket.
@@ -263,6 +286,25 @@ export class Call {
263
286
  await started;
264
287
  }
265
288
 
289
+ /**
290
+ * Mutes or unmutes the microphone. Can be called before the call starts.
291
+ * @param muted - Whether the microphone should be muted.
292
+ */
293
+ setMicrophoneMuted(muted: boolean) {
294
+ this._micMuted = muted;
295
+ for (const track of this._localStream?.getAudioTracks() ?? []) {
296
+ track.enabled = !muted;
297
+ }
298
+ }
299
+
300
+ /**
301
+ * Mutes or unmutes playback of the remote audio. Can be called before the call starts.
302
+ * @param muted - Whether the remote audio should be muted.
303
+ */
304
+ setRemoteMuted(muted: boolean) {
305
+ this._remoteAudio.muted = muted;
306
+ }
307
+
266
308
  /**
267
309
  * Sends a message to the agent, delivered to its `on_webrtc_message` handler.
268
310
  * Messages sent before the message channel is ready are queued.
@@ -426,6 +468,7 @@ export class Call {
426
468
  }
427
469
  };
428
470
  for (const track of this._localStream.getAudioTracks()) {
471
+ track.enabled = !this._micMuted;
429
472
  this._peerConnection.addTrack(track);
430
473
  }
431
474