@craftedxp/voice-js 0.5.4 → 0.6.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/browser.d.mts +65 -1
- package/dist/browser.d.ts +467 -502
- package/dist/browser.js +921 -836
- package/dist/browser.js.map +1 -1
- package/dist/browser.mjs +93 -1
- package/dist/browser.mjs.map +1 -1
- package/dist/embed.iife.js +67 -23358
- package/dist/node.d.mts +63 -0
- package/dist/node.d.ts +462 -483
- package/dist/node.js +453 -467
- package/dist/node.js.map +1 -1
- package/dist/node.mjs.map +1 -1
- package/package.json +2 -1
package/dist/browser.js
CHANGED
|
@@ -1,26 +1,24 @@
|
|
|
1
|
-
|
|
2
|
-
var __defProp = Object.defineProperty
|
|
3
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor
|
|
4
|
-
var __getOwnPropNames = Object.getOwnPropertyNames
|
|
5
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
6
|
var __export = (target, all) => {
|
|
7
|
-
for (var name in all)
|
|
8
|
-
}
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
9
10
|
var __copyProps = (to, from, except, desc) => {
|
|
10
|
-
if (
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
12
|
for (let key of __getOwnPropNames(from))
|
|
12
13
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
-
__defProp(to, key, {
|
|
14
|
-
get: () => from[key],
|
|
15
|
-
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable,
|
|
16
|
-
})
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
15
|
}
|
|
18
|
-
return to
|
|
19
|
-
}
|
|
20
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({},
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
21
19
|
|
|
22
20
|
// src/browser.ts
|
|
23
|
-
var browser_exports = {}
|
|
21
|
+
var browser_exports = {};
|
|
24
22
|
__export(browser_exports, {
|
|
25
23
|
buildWsUrl: () => buildWsUrl,
|
|
26
24
|
configureVoiceClient: () => configureVoiceClient,
|
|
@@ -31,63 +29,57 @@ __export(browser_exports, {
|
|
|
31
29
|
handleServerMessage: () => handleServerMessage,
|
|
32
30
|
joinRoom: () => joinRoom,
|
|
33
31
|
parseIncomingCall: () => parseIncomingCall,
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
startTextSession: () => startTextSession
|
|
33
|
+
});
|
|
34
|
+
module.exports = __toCommonJS(browser_exports);
|
|
36
35
|
|
|
37
36
|
// src/config.ts
|
|
38
37
|
function normalizeConfig(config) {
|
|
39
|
-
if (!config) throw new Error(
|
|
40
|
-
if (
|
|
38
|
+
if (!config) throw new Error("configureVoiceClient: config is required");
|
|
39
|
+
if ("apiKey" in config) {
|
|
41
40
|
throw new Error(
|
|
42
|
-
|
|
43
|
-
)
|
|
41
|
+
"configureVoiceClient: `apiKey` is no longer supported. Embedding sk_ in JS code ships server-grade credentials to every client. Pass `fetchToken: async ({ agentId }) => { /* call YOUR backend mint */ }` instead \u2014 see the @craftedxp/voice-js README for the migration recipe."
|
|
42
|
+
);
|
|
44
43
|
}
|
|
45
44
|
if (!config.apiBase) {
|
|
46
|
-
throw new Error(
|
|
45
|
+
throw new Error("configureVoiceClient: apiBase is required");
|
|
47
46
|
}
|
|
48
|
-
if (typeof config.fetchToken !==
|
|
49
|
-
throw new Error(
|
|
47
|
+
if (typeof config.fetchToken !== "function") {
|
|
48
|
+
throw new Error("configureVoiceClient: fetchToken must be a function");
|
|
50
49
|
}
|
|
51
50
|
return {
|
|
52
51
|
...config,
|
|
53
|
-
apiBase: config.apiBase.replace(/\/+$/,
|
|
54
|
-
}
|
|
52
|
+
apiBase: config.apiBase.replace(/\/+$/, "")
|
|
53
|
+
};
|
|
55
54
|
}
|
|
56
55
|
function mergeStartCallContext(factory, call) {
|
|
57
|
-
const context =
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
: void 0
|
|
61
|
-
const metadata =
|
|
62
|
-
factory.defaultMetadata || call.metadata
|
|
63
|
-
? { ...(factory.defaultMetadata ?? {}), ...(call.metadata ?? {}) }
|
|
64
|
-
: void 0
|
|
65
|
-
return { context, metadata }
|
|
56
|
+
const context = factory.defaultContext || call.context ? { ...factory.defaultContext ?? {}, ...call.context ?? {} } : void 0;
|
|
57
|
+
const metadata = factory.defaultMetadata || call.metadata ? { ...factory.defaultMetadata ?? {}, ...call.metadata ?? {} } : void 0;
|
|
58
|
+
return { context, metadata };
|
|
66
59
|
}
|
|
67
60
|
|
|
68
61
|
// src/worklets/mic-downsampler.worklet.js
|
|
69
|
-
var mic_downsampler_worklet_default =
|
|
70
|
-
"// AudioWorklet \u2014 runs off the main thread in the audio rendering graph.\n//\n// Input: Float32 samples at the AudioContext's native sampleRate (typically\n// 48000 Hz on desktop, 44100 Hz on some iOS devices).\n// Output: 16 kHz mono Int16 PCM, shipped to the main thread via\n// `port.postMessage(ArrayBuffer, [ArrayBuffer])` (transferred, not copied).\n//\n// Why AudioWorklet instead of ScriptProcessorNode: ScriptProcessorNode is\n// deprecated + main-thread-bound, so any JS jank produces audible audio\n// glitches. AudioWorklet's `process()` runs on the audio rendering thread\n// at the graph's block cadence (128 frames by default) and backpressures\n// via returning `true` / `false`.\n//\n// This file is loaded as text (see tsup.config.ts loader) and registered\n// at runtime via `audioWorklet.addModule(blobUrl)`.\n\nclass MicDownsampler extends AudioWorkletProcessor {\n constructor() {\n super()\n // Target sample rate for STT. Matches Deepgram Nova-3 + the platform's\n // server-side SAMPLE_RATE constant in AgentCallHandler.\n this.targetRate = 16000\n // Accumulator for the downsample. We collect incoming samples and emit\n // an Int16 chunk when we've accumulated ~1024 target-rate samples\n // (~64 ms at 16 kHz) \u2014 matches the mobile SDK's chunk size so both\n // platforms have the same server-side framing.\n this.outputFrames = 1024\n this.acc = []\n // Running index used for fractional resampling.\n this.readCursor = 0\n }\n\n // `inputs[0][0]` = first channel of first input. 128 Float32 samples per\n // call at the context's sampleRate. Return true = keep processing.\n process(inputs) {\n const input = inputs[0]\n if (!input || input.length === 0) return true\n const channel = input[0]\n if (!channel || channel.length === 0) return true\n\n const ctxRate = sampleRate // global inside AudioWorkletProcessor\n const ratio = ctxRate / this.targetRate\n\n // Simple linear-interp downsample. For 48000 \u2192 16000 that's 3:1, which\n // linear handles fine for voice. Anti-alias filtering would be\n // theoretically better but inaudible for speech.\n for (let i = 0; i < channel.length; i++) {\n this.acc.push(channel[i])\n }\n\n while (this.acc.length - this.readCursor >= ratio * this.outputFrames) {\n const out = new Int16Array(this.outputFrames)\n let readIdx = this.readCursor\n for (let i = 0; i < this.outputFrames; i++) {\n // Linear interp between floor(readIdx) and ceil(readIdx)\n const low = Math.floor(readIdx)\n const high = Math.min(low + 1, this.acc.length - 1)\n const frac = readIdx - low\n const sample = this.acc[low] * (1 - frac) + this.acc[high] * frac\n // Clip + convert to int16\n const clipped = Math.max(-1, Math.min(1, sample))\n out[i] = clipped < 0 ? clipped * 0x8000 : clipped * 0x7fff\n readIdx += ratio\n }\n // Transfer the ArrayBuffer (zero-copy) to the main thread.\n this.port.postMessage(out.buffer, [out.buffer])\n this.readCursor = readIdx\n }\n\n // Garbage-collect the consumed portion of `acc` every so often so it\n // doesn't grow without bound. Leave ~one chunk of headroom.\n if (this.readCursor > ratio * this.outputFrames) {\n this.acc = this.acc.slice(Math.floor(this.readCursor))\n this.readCursor -= Math.floor(this.readCursor)\n }\n\n return true\n }\n}\n\nregisterProcessor('mic-downsampler', MicDownsampler)\n"
|
|
62
|
+
var mic_downsampler_worklet_default = "// AudioWorklet \u2014 runs off the main thread in the audio rendering graph.\n//\n// Input: Float32 samples at the AudioContext's native sampleRate (typically\n// 48000 Hz on desktop, 44100 Hz on some iOS devices).\n// Output: 16 kHz mono Int16 PCM, shipped to the main thread via\n// `port.postMessage(ArrayBuffer, [ArrayBuffer])` (transferred, not copied).\n//\n// Why AudioWorklet instead of ScriptProcessorNode: ScriptProcessorNode is\n// deprecated + main-thread-bound, so any JS jank produces audible audio\n// glitches. AudioWorklet's `process()` runs on the audio rendering thread\n// at the graph's block cadence (128 frames by default) and backpressures\n// via returning `true` / `false`.\n//\n// This file is loaded as text (see tsup.config.ts loader) and registered\n// at runtime via `audioWorklet.addModule(blobUrl)`.\n\nclass MicDownsampler extends AudioWorkletProcessor {\n constructor() {\n super()\n // Target sample rate for STT. Matches Deepgram Nova-3 + the platform's\n // server-side SAMPLE_RATE constant in AgentCallHandler.\n this.targetRate = 16000\n // Accumulator for the downsample. We collect incoming samples and emit\n // an Int16 chunk when we've accumulated ~1024 target-rate samples\n // (~64 ms at 16 kHz) \u2014 matches the mobile SDK's chunk size so both\n // platforms have the same server-side framing.\n this.outputFrames = 1024\n this.acc = []\n // Running index used for fractional resampling.\n this.readCursor = 0\n }\n\n // `inputs[0][0]` = first channel of first input. 128 Float32 samples per\n // call at the context's sampleRate. Return true = keep processing.\n process(inputs) {\n const input = inputs[0]\n if (!input || input.length === 0) return true\n const channel = input[0]\n if (!channel || channel.length === 0) return true\n\n const ctxRate = sampleRate // global inside AudioWorkletProcessor\n const ratio = ctxRate / this.targetRate\n\n // Simple linear-interp downsample. For 48000 \u2192 16000 that's 3:1, which\n // linear handles fine for voice. Anti-alias filtering would be\n // theoretically better but inaudible for speech.\n for (let i = 0; i < channel.length; i++) {\n this.acc.push(channel[i])\n }\n\n while (this.acc.length - this.readCursor >= ratio * this.outputFrames) {\n const out = new Int16Array(this.outputFrames)\n let readIdx = this.readCursor\n for (let i = 0; i < this.outputFrames; i++) {\n // Linear interp between floor(readIdx) and ceil(readIdx)\n const low = Math.floor(readIdx)\n const high = Math.min(low + 1, this.acc.length - 1)\n const frac = readIdx - low\n const sample = this.acc[low] * (1 - frac) + this.acc[high] * frac\n // Clip + convert to int16\n const clipped = Math.max(-1, Math.min(1, sample))\n out[i] = clipped < 0 ? clipped * 0x8000 : clipped * 0x7fff\n readIdx += ratio\n }\n // Transfer the ArrayBuffer (zero-copy) to the main thread.\n this.port.postMessage(out.buffer, [out.buffer])\n this.readCursor = readIdx\n }\n\n // Garbage-collect the consumed portion of `acc` every so often so it\n // doesn't grow without bound. Leave ~one chunk of headroom.\n if (this.readCursor > ratio * this.outputFrames) {\n this.acc = this.acc.slice(Math.floor(this.readCursor))\n this.readCursor -= Math.floor(this.readCursor)\n }\n\n return true\n }\n}\n\nregisterProcessor('mic-downsampler', MicDownsampler)\n";
|
|
71
63
|
|
|
72
64
|
// src/AudioCapture.ts
|
|
73
|
-
var VOLUME_INTERVAL_MS = 100
|
|
65
|
+
var VOLUME_INTERVAL_MS = 100;
|
|
74
66
|
var createAudioCapture = (options) => {
|
|
75
|
-
let audioContext = null
|
|
76
|
-
let mediaStream = null
|
|
77
|
-
let sourceNode = null
|
|
78
|
-
let workletNode = null
|
|
79
|
-
let analyser = null
|
|
80
|
-
let volumeTimer = null
|
|
81
|
-
let muted = false
|
|
82
|
-
let capturing = false
|
|
67
|
+
let audioContext = null;
|
|
68
|
+
let mediaStream = null;
|
|
69
|
+
let sourceNode = null;
|
|
70
|
+
let workletNode = null;
|
|
71
|
+
let analyser = null;
|
|
72
|
+
let volumeTimer = null;
|
|
73
|
+
let muted = false;
|
|
74
|
+
let capturing = false;
|
|
83
75
|
const computeRms = (buf) => {
|
|
84
|
-
let sum = 0
|
|
85
|
-
for (let i = 0; i < buf.length; i++) sum += buf[i] * buf[i]
|
|
86
|
-
const rms = Math.sqrt(sum / buf.length)
|
|
87
|
-
return Math.min(1, rms * 1.8)
|
|
88
|
-
}
|
|
76
|
+
let sum = 0;
|
|
77
|
+
for (let i = 0; i < buf.length; i++) sum += buf[i] * buf[i];
|
|
78
|
+
const rms = Math.sqrt(sum / buf.length);
|
|
79
|
+
return Math.min(1, rms * 1.8);
|
|
80
|
+
};
|
|
89
81
|
const start = async () => {
|
|
90
|
-
if (capturing) return
|
|
82
|
+
if (capturing) return;
|
|
91
83
|
try {
|
|
92
84
|
mediaStream = await navigator.mediaDevices.getUserMedia({
|
|
93
85
|
audio: {
|
|
@@ -98,748 +90,751 @@ var createAudioCapture = (options) => {
|
|
|
98
90
|
echoCancellation: true,
|
|
99
91
|
noiseSuppression: true,
|
|
100
92
|
autoGainControl: true,
|
|
101
|
-
channelCount: 1
|
|
102
|
-
}
|
|
103
|
-
})
|
|
104
|
-
audioContext = new AudioContext()
|
|
105
|
-
if (audioContext.state ===
|
|
106
|
-
const blob = new Blob([mic_downsampler_worklet_default], { type:
|
|
107
|
-
const url = URL.createObjectURL(blob)
|
|
93
|
+
channelCount: 1
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
audioContext = new AudioContext();
|
|
97
|
+
if (audioContext.state === "suspended") await audioContext.resume();
|
|
98
|
+
const blob = new Blob([mic_downsampler_worklet_default], { type: "application/javascript" });
|
|
99
|
+
const url = URL.createObjectURL(blob);
|
|
108
100
|
try {
|
|
109
|
-
await audioContext.audioWorklet.addModule(url)
|
|
101
|
+
await audioContext.audioWorklet.addModule(url);
|
|
110
102
|
} finally {
|
|
111
|
-
URL.revokeObjectURL(url)
|
|
103
|
+
URL.revokeObjectURL(url);
|
|
112
104
|
}
|
|
113
|
-
sourceNode = audioContext.createMediaStreamSource(mediaStream)
|
|
114
|
-
workletNode = new AudioWorkletNode(audioContext,
|
|
105
|
+
sourceNode = audioContext.createMediaStreamSource(mediaStream);
|
|
106
|
+
workletNode = new AudioWorkletNode(audioContext, "mic-downsampler");
|
|
115
107
|
workletNode.port.onmessage = (event) => {
|
|
116
|
-
if (muted) return
|
|
117
|
-
options.onChunk(event.data)
|
|
118
|
-
}
|
|
108
|
+
if (muted) return;
|
|
109
|
+
options.onChunk(event.data);
|
|
110
|
+
};
|
|
119
111
|
if (options.onVolume) {
|
|
120
|
-
analyser = audioContext.createAnalyser()
|
|
121
|
-
analyser.fftSize = 256
|
|
122
|
-
sourceNode.connect(analyser)
|
|
123
|
-
const buf = new Float32Array(analyser.fftSize)
|
|
112
|
+
analyser = audioContext.createAnalyser();
|
|
113
|
+
analyser.fftSize = 256;
|
|
114
|
+
sourceNode.connect(analyser);
|
|
115
|
+
const buf = new Float32Array(analyser.fftSize);
|
|
124
116
|
volumeTimer = setInterval(() => {
|
|
125
|
-
if (!analyser) return
|
|
126
|
-
analyser.getFloatTimeDomainData(buf)
|
|
127
|
-
options.onVolume?.(computeRms(buf))
|
|
128
|
-
}, VOLUME_INTERVAL_MS)
|
|
117
|
+
if (!analyser) return;
|
|
118
|
+
analyser.getFloatTimeDomainData(buf);
|
|
119
|
+
options.onVolume?.(computeRms(buf));
|
|
120
|
+
}, VOLUME_INTERVAL_MS);
|
|
129
121
|
}
|
|
130
|
-
sourceNode.connect(workletNode)
|
|
131
|
-
const sink = audioContext.createGain()
|
|
132
|
-
sink.gain.value = 0
|
|
133
|
-
workletNode.connect(sink).connect(audioContext.destination)
|
|
134
|
-
capturing = true
|
|
122
|
+
sourceNode.connect(workletNode);
|
|
123
|
+
const sink = audioContext.createGain();
|
|
124
|
+
sink.gain.value = 0;
|
|
125
|
+
workletNode.connect(sink).connect(audioContext.destination);
|
|
126
|
+
capturing = true;
|
|
135
127
|
} catch (err) {
|
|
136
|
-
const wrapped =
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
throw wrapped
|
|
128
|
+
const wrapped = err instanceof Error ? err : new Error(typeof err === "string" ? err : "capture failed");
|
|
129
|
+
options.onError?.(wrapped);
|
|
130
|
+
throw wrapped;
|
|
140
131
|
}
|
|
141
|
-
}
|
|
132
|
+
};
|
|
142
133
|
const stop = () => {
|
|
143
|
-
if (!capturing) return
|
|
144
|
-
capturing = false
|
|
134
|
+
if (!capturing) return;
|
|
135
|
+
capturing = false;
|
|
145
136
|
if (volumeTimer) {
|
|
146
|
-
clearInterval(volumeTimer)
|
|
147
|
-
volumeTimer = null
|
|
137
|
+
clearInterval(volumeTimer);
|
|
138
|
+
volumeTimer = null;
|
|
148
139
|
}
|
|
149
140
|
try {
|
|
150
|
-
workletNode?.disconnect()
|
|
151
|
-
analyser?.disconnect()
|
|
152
|
-
sourceNode?.disconnect()
|
|
153
|
-
} catch {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
141
|
+
workletNode?.disconnect();
|
|
142
|
+
analyser?.disconnect();
|
|
143
|
+
sourceNode?.disconnect();
|
|
144
|
+
} catch {
|
|
145
|
+
}
|
|
146
|
+
workletNode = null;
|
|
147
|
+
analyser = null;
|
|
148
|
+
sourceNode = null;
|
|
157
149
|
if (mediaStream) {
|
|
158
|
-
for (const track of mediaStream.getTracks()) track.stop()
|
|
159
|
-
mediaStream = null
|
|
150
|
+
for (const track of mediaStream.getTracks()) track.stop();
|
|
151
|
+
mediaStream = null;
|
|
160
152
|
}
|
|
161
|
-
if (audioContext && audioContext.state !==
|
|
162
|
-
void audioContext.close().catch(() => void 0)
|
|
153
|
+
if (audioContext && audioContext.state !== "closed") {
|
|
154
|
+
void audioContext.close().catch(() => void 0);
|
|
163
155
|
}
|
|
164
|
-
audioContext = null
|
|
165
|
-
}
|
|
156
|
+
audioContext = null;
|
|
157
|
+
};
|
|
166
158
|
return {
|
|
167
159
|
start,
|
|
168
160
|
stop,
|
|
169
161
|
mute: (v) => {
|
|
170
|
-
muted = v
|
|
162
|
+
muted = v;
|
|
171
163
|
},
|
|
172
|
-
isCapturing: () => capturing
|
|
173
|
-
}
|
|
174
|
-
}
|
|
164
|
+
isCapturing: () => capturing
|
|
165
|
+
};
|
|
166
|
+
};
|
|
175
167
|
|
|
176
168
|
// src/AudioPlayback.ts
|
|
177
|
-
var DEFAULT_SAMPLE_RATE = 16e3
|
|
178
|
-
var VOLUME_INTERVAL_MS2 = 100
|
|
169
|
+
var DEFAULT_SAMPLE_RATE = 16e3;
|
|
170
|
+
var VOLUME_INTERVAL_MS2 = 100;
|
|
179
171
|
var createAudioPlayback = (options = {}) => {
|
|
180
|
-
const sampleRate = options.sampleRate ?? DEFAULT_SAMPLE_RATE
|
|
181
|
-
let audioContext = null
|
|
182
|
-
let gainNode = null
|
|
183
|
-
let analyser = null
|
|
184
|
-
let volumeTimer = null
|
|
185
|
-
let nextStartTime = 0
|
|
186
|
-
let scheduledNodes = []
|
|
187
|
-
let speaking = false
|
|
172
|
+
const sampleRate = options.sampleRate ?? DEFAULT_SAMPLE_RATE;
|
|
173
|
+
let audioContext = null;
|
|
174
|
+
let gainNode = null;
|
|
175
|
+
let analyser = null;
|
|
176
|
+
let volumeTimer = null;
|
|
177
|
+
let nextStartTime = 0;
|
|
178
|
+
let scheduledNodes = [];
|
|
179
|
+
let speaking = false;
|
|
188
180
|
const ensureContext = async () => {
|
|
189
181
|
if (audioContext) {
|
|
190
|
-
if (audioContext.state ===
|
|
191
|
-
return
|
|
182
|
+
if (audioContext.state === "suspended") await audioContext.resume();
|
|
183
|
+
return;
|
|
192
184
|
}
|
|
193
|
-
audioContext = new AudioContext({ sampleRate })
|
|
194
|
-
gainNode = audioContext.createGain()
|
|
185
|
+
audioContext = new AudioContext({ sampleRate });
|
|
186
|
+
gainNode = audioContext.createGain();
|
|
195
187
|
if (options.onVolume) {
|
|
196
|
-
analyser = audioContext.createAnalyser()
|
|
197
|
-
analyser.fftSize = 256
|
|
198
|
-
gainNode.connect(analyser)
|
|
199
|
-
const buf = new Float32Array(analyser.fftSize)
|
|
188
|
+
analyser = audioContext.createAnalyser();
|
|
189
|
+
analyser.fftSize = 256;
|
|
190
|
+
gainNode.connect(analyser);
|
|
191
|
+
const buf = new Float32Array(analyser.fftSize);
|
|
200
192
|
volumeTimer = setInterval(() => {
|
|
201
|
-
if (!analyser) return
|
|
202
|
-
analyser.getFloatTimeDomainData(buf)
|
|
203
|
-
let sum = 0
|
|
204
|
-
for (let i = 0; i < buf.length; i++) sum += buf[i] * buf[i]
|
|
205
|
-
const rms = Math.sqrt(sum / buf.length)
|
|
206
|
-
options.onVolume?.(Math.min(1, rms * 1.8))
|
|
207
|
-
}, VOLUME_INTERVAL_MS2)
|
|
193
|
+
if (!analyser) return;
|
|
194
|
+
analyser.getFloatTimeDomainData(buf);
|
|
195
|
+
let sum = 0;
|
|
196
|
+
for (let i = 0; i < buf.length; i++) sum += buf[i] * buf[i];
|
|
197
|
+
const rms = Math.sqrt(sum / buf.length);
|
|
198
|
+
options.onVolume?.(Math.min(1, rms * 1.8));
|
|
199
|
+
}, VOLUME_INTERVAL_MS2);
|
|
208
200
|
}
|
|
209
|
-
gainNode.connect(audioContext.destination)
|
|
210
|
-
nextStartTime = audioContext.currentTime
|
|
211
|
-
}
|
|
201
|
+
gainNode.connect(audioContext.destination);
|
|
202
|
+
nextStartTime = audioContext.currentTime;
|
|
203
|
+
};
|
|
212
204
|
const setSpeaking = (v) => {
|
|
213
|
-
if (v === speaking) return
|
|
214
|
-
speaking = v
|
|
215
|
-
options.onSpeakingChange?.(v)
|
|
216
|
-
}
|
|
205
|
+
if (v === speaking) return;
|
|
206
|
+
speaking = v;
|
|
207
|
+
options.onSpeakingChange?.(v);
|
|
208
|
+
};
|
|
217
209
|
const pruneFinished = () => {
|
|
218
|
-
const now = audioContext?.currentTime ?? 0
|
|
210
|
+
const now = audioContext?.currentTime ?? 0;
|
|
219
211
|
scheduledNodes = scheduledNodes.filter((n) => {
|
|
220
|
-
const node = n
|
|
221
|
-
return (node._endsAt ?? 0) > now
|
|
222
|
-
})
|
|
223
|
-
if (scheduledNodes.length === 0) setSpeaking(false)
|
|
224
|
-
}
|
|
212
|
+
const node = n;
|
|
213
|
+
return (node._endsAt ?? 0) > now;
|
|
214
|
+
});
|
|
215
|
+
if (scheduledNodes.length === 0) setSpeaking(false);
|
|
216
|
+
};
|
|
225
217
|
const enqueue = (pcm) => {
|
|
226
218
|
if (!audioContext) {
|
|
227
|
-
void ensureContext().then(() => enqueue(pcm))
|
|
228
|
-
return
|
|
219
|
+
void ensureContext().then(() => enqueue(pcm));
|
|
220
|
+
return;
|
|
229
221
|
}
|
|
230
|
-
if (!audioContext || !gainNode) return
|
|
231
|
-
const int16 = new Int16Array(pcm)
|
|
232
|
-
if (int16.length === 0) return
|
|
233
|
-
const audioBuffer = audioContext.createBuffer(1, int16.length, sampleRate)
|
|
234
|
-
const float32 = audioBuffer.getChannelData(0)
|
|
222
|
+
if (!audioContext || !gainNode) return;
|
|
223
|
+
const int16 = new Int16Array(pcm);
|
|
224
|
+
if (int16.length === 0) return;
|
|
225
|
+
const audioBuffer = audioContext.createBuffer(1, int16.length, sampleRate);
|
|
226
|
+
const float32 = audioBuffer.getChannelData(0);
|
|
235
227
|
for (let i = 0; i < int16.length; i++) {
|
|
236
|
-
float32[i] = int16[i] / 32768
|
|
228
|
+
float32[i] = int16[i] / 32768;
|
|
237
229
|
}
|
|
238
|
-
const node = audioContext.createBufferSource()
|
|
239
|
-
node.buffer = audioBuffer
|
|
240
|
-
node.connect(gainNode)
|
|
241
|
-
const now = audioContext.currentTime
|
|
242
|
-
const startAt = Math.max(now, nextStartTime)
|
|
243
|
-
node.start(startAt)
|
|
244
|
-
const duration = int16.length / sampleRate
|
|
245
|
-
node._endsAt = startAt + duration
|
|
246
|
-
nextStartTime = startAt + duration
|
|
247
|
-
scheduledNodes.push(node)
|
|
248
|
-
setSpeaking(true)
|
|
249
|
-
node.onended = () => pruneFinished()
|
|
250
|
-
}
|
|
230
|
+
const node = audioContext.createBufferSource();
|
|
231
|
+
node.buffer = audioBuffer;
|
|
232
|
+
node.connect(gainNode);
|
|
233
|
+
const now = audioContext.currentTime;
|
|
234
|
+
const startAt = Math.max(now, nextStartTime);
|
|
235
|
+
node.start(startAt);
|
|
236
|
+
const duration = int16.length / sampleRate;
|
|
237
|
+
node._endsAt = startAt + duration;
|
|
238
|
+
nextStartTime = startAt + duration;
|
|
239
|
+
scheduledNodes.push(node);
|
|
240
|
+
setSpeaking(true);
|
|
241
|
+
node.onended = () => pruneFinished();
|
|
242
|
+
};
|
|
251
243
|
const flush = () => {
|
|
252
|
-
if (!audioContext || !gainNode) return
|
|
244
|
+
if (!audioContext || !gainNode) return;
|
|
253
245
|
for (const node of scheduledNodes) {
|
|
254
246
|
try {
|
|
255
|
-
node.stop()
|
|
256
|
-
} catch {
|
|
247
|
+
node.stop();
|
|
248
|
+
} catch {
|
|
249
|
+
}
|
|
257
250
|
}
|
|
258
|
-
scheduledNodes = []
|
|
259
|
-
gainNode.disconnect()
|
|
260
|
-
gainNode = audioContext.createGain()
|
|
251
|
+
scheduledNodes = [];
|
|
252
|
+
gainNode.disconnect();
|
|
253
|
+
gainNode = audioContext.createGain();
|
|
261
254
|
if (analyser) {
|
|
262
|
-
analyser.disconnect()
|
|
263
|
-
gainNode.connect(analyser)
|
|
255
|
+
analyser.disconnect();
|
|
256
|
+
gainNode.connect(analyser);
|
|
264
257
|
}
|
|
265
|
-
gainNode.connect(audioContext.destination)
|
|
266
|
-
nextStartTime = audioContext.currentTime
|
|
267
|
-
setSpeaking(false)
|
|
268
|
-
}
|
|
258
|
+
gainNode.connect(audioContext.destination);
|
|
259
|
+
nextStartTime = audioContext.currentTime;
|
|
260
|
+
setSpeaking(false);
|
|
261
|
+
};
|
|
269
262
|
const close = () => {
|
|
270
|
-
flush()
|
|
263
|
+
flush();
|
|
271
264
|
if (volumeTimer) {
|
|
272
|
-
clearInterval(volumeTimer)
|
|
273
|
-
volumeTimer = null
|
|
265
|
+
clearInterval(volumeTimer);
|
|
266
|
+
volumeTimer = null;
|
|
274
267
|
}
|
|
275
|
-
if (audioContext && audioContext.state !==
|
|
276
|
-
void audioContext.close().catch(() => void 0)
|
|
268
|
+
if (audioContext && audioContext.state !== "closed") {
|
|
269
|
+
void audioContext.close().catch(() => void 0);
|
|
277
270
|
}
|
|
278
|
-
audioContext = null
|
|
279
|
-
gainNode = null
|
|
280
|
-
analyser = null
|
|
281
|
-
}
|
|
271
|
+
audioContext = null;
|
|
272
|
+
gainNode = null;
|
|
273
|
+
analyser = null;
|
|
274
|
+
};
|
|
282
275
|
const resume = async () => {
|
|
283
|
-
await ensureContext()
|
|
284
|
-
}
|
|
285
|
-
return { enqueue, flush, close, resume }
|
|
286
|
-
}
|
|
276
|
+
await ensureContext();
|
|
277
|
+
};
|
|
278
|
+
return { enqueue, flush, close, resume };
|
|
279
|
+
};
|
|
287
280
|
|
|
288
281
|
// src/ReconnectingWebSocket.ts
|
|
289
|
-
var READYSTATE_OPEN = 1
|
|
290
|
-
var READYSTATE_CLOSED = 3
|
|
282
|
+
var READYSTATE_OPEN = 1;
|
|
283
|
+
var READYSTATE_CLOSED = 3;
|
|
291
284
|
var createReconnectingWebSocket = (options, onEvent) => {
|
|
292
|
-
const maxRetries = options.maxRetries ?? 3
|
|
293
|
-
const initialBackoff = options.initialBackoffMs ?? 500
|
|
294
|
-
const maxBackoff = options.maxBackoffMs ?? 8e3
|
|
295
|
-
let ws = null
|
|
296
|
-
let intentionalClose = false
|
|
297
|
-
let retries = 0
|
|
298
|
-
let backoff = initialBackoff
|
|
299
|
-
let reconnectTimer = null
|
|
285
|
+
const maxRetries = options.maxRetries ?? 3;
|
|
286
|
+
const initialBackoff = options.initialBackoffMs ?? 500;
|
|
287
|
+
const maxBackoff = options.maxBackoffMs ?? 8e3;
|
|
288
|
+
let ws = null;
|
|
289
|
+
let intentionalClose = false;
|
|
290
|
+
let retries = 0;
|
|
291
|
+
let backoff = initialBackoff;
|
|
292
|
+
let reconnectTimer = null;
|
|
300
293
|
const openOnce = () => {
|
|
301
|
-
ws = options.wsFactory(options.url)
|
|
302
|
-
ws.binaryType =
|
|
294
|
+
ws = options.wsFactory(options.url);
|
|
295
|
+
ws.binaryType = "arraybuffer";
|
|
303
296
|
ws.onopen = () => {
|
|
304
|
-
if (retries === 0) onEvent({ type:
|
|
305
|
-
else onEvent({ type:
|
|
306
|
-
retries = 0
|
|
307
|
-
backoff = initialBackoff
|
|
308
|
-
}
|
|
297
|
+
if (retries === 0) onEvent({ type: "open" });
|
|
298
|
+
else onEvent({ type: "reconnected" });
|
|
299
|
+
retries = 0;
|
|
300
|
+
backoff = initialBackoff;
|
|
301
|
+
};
|
|
309
302
|
ws.onmessage = (ev) => {
|
|
310
|
-
onEvent({ type:
|
|
311
|
-
}
|
|
303
|
+
onEvent({ type: "message", data: ev.data });
|
|
304
|
+
};
|
|
312
305
|
ws.onerror = () => {
|
|
313
|
-
onEvent({ type:
|
|
314
|
-
}
|
|
306
|
+
onEvent({ type: "error", error: new Error("WebSocket error") });
|
|
307
|
+
};
|
|
315
308
|
ws.onclose = (ev) => {
|
|
316
|
-
ws = null
|
|
317
|
-
const shouldRetry = !intentionalClose && retries < maxRetries
|
|
309
|
+
ws = null;
|
|
310
|
+
const shouldRetry = !intentionalClose && retries < maxRetries;
|
|
318
311
|
if (!shouldRetry) {
|
|
319
312
|
onEvent({
|
|
320
|
-
type:
|
|
313
|
+
type: "close",
|
|
321
314
|
code: ev.code,
|
|
322
315
|
reason: ev.reason,
|
|
323
|
-
permanent: true
|
|
324
|
-
})
|
|
325
|
-
return
|
|
316
|
+
permanent: true
|
|
317
|
+
});
|
|
318
|
+
return;
|
|
326
319
|
}
|
|
327
320
|
onEvent({
|
|
328
|
-
type:
|
|
321
|
+
type: "close",
|
|
329
322
|
code: ev.code,
|
|
330
323
|
reason: ev.reason,
|
|
331
|
-
permanent: false
|
|
332
|
-
})
|
|
333
|
-
retries
|
|
334
|
-
const delay = Math.min(backoff, maxBackoff)
|
|
335
|
-
backoff = Math.min(backoff * 2, maxBackoff)
|
|
336
|
-
reconnectTimer = setTimeout(openOnce, delay)
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
openOnce()
|
|
324
|
+
permanent: false
|
|
325
|
+
});
|
|
326
|
+
retries++;
|
|
327
|
+
const delay = Math.min(backoff, maxBackoff);
|
|
328
|
+
backoff = Math.min(backoff * 2, maxBackoff);
|
|
329
|
+
reconnectTimer = setTimeout(openOnce, delay);
|
|
330
|
+
};
|
|
331
|
+
};
|
|
332
|
+
openOnce();
|
|
340
333
|
return {
|
|
341
334
|
send: (data) => {
|
|
342
|
-
if (ws && ws.readyState === READYSTATE_OPEN) ws.send(data)
|
|
335
|
+
if (ws && ws.readyState === READYSTATE_OPEN) ws.send(data);
|
|
343
336
|
},
|
|
344
|
-
close: (code = 1e3, reason =
|
|
345
|
-
intentionalClose = true
|
|
337
|
+
close: (code = 1e3, reason = "client-requested") => {
|
|
338
|
+
intentionalClose = true;
|
|
346
339
|
if (reconnectTimer) {
|
|
347
|
-
clearTimeout(reconnectTimer)
|
|
348
|
-
reconnectTimer = null
|
|
340
|
+
clearTimeout(reconnectTimer);
|
|
341
|
+
reconnectTimer = null;
|
|
349
342
|
}
|
|
350
343
|
try {
|
|
351
|
-
ws?.close(code, reason)
|
|
352
|
-
} catch {
|
|
344
|
+
ws?.close(code, reason);
|
|
345
|
+
} catch {
|
|
346
|
+
}
|
|
353
347
|
},
|
|
354
|
-
readyState: () => ws?.readyState ?? READYSTATE_CLOSED
|
|
355
|
-
}
|
|
356
|
-
}
|
|
348
|
+
readyState: () => ws?.readyState ?? READYSTATE_CLOSED
|
|
349
|
+
};
|
|
350
|
+
};
|
|
357
351
|
|
|
358
352
|
// src/protocol.ts
|
|
359
353
|
var createProtocolState = () => ({
|
|
360
|
-
state:
|
|
354
|
+
state: "idle",
|
|
361
355
|
transcript: [],
|
|
362
356
|
agentBubbleId: null,
|
|
363
357
|
idCounter: 0,
|
|
364
|
-
endReason: null
|
|
365
|
-
})
|
|
358
|
+
endReason: null
|
|
359
|
+
});
|
|
366
360
|
var mapEndReason = (raw) => {
|
|
367
|
-
if (raw ===
|
|
368
|
-
if (raw ===
|
|
369
|
-
if (raw ===
|
|
370
|
-
return
|
|
371
|
-
}
|
|
361
|
+
if (raw === "agent_ended") return "agent_ended";
|
|
362
|
+
if (raw === "caller_hung_up") return "user_hangup";
|
|
363
|
+
if (raw === "silence_timeout" || raw === "max_duration") return "timeout";
|
|
364
|
+
return "error";
|
|
365
|
+
};
|
|
372
366
|
function handleServerMessage(raw, state, cb) {
|
|
373
|
-
let msg
|
|
367
|
+
let msg;
|
|
374
368
|
try {
|
|
375
|
-
msg = JSON.parse(raw)
|
|
369
|
+
msg = JSON.parse(raw);
|
|
376
370
|
} catch {
|
|
377
|
-
return
|
|
371
|
+
return;
|
|
378
372
|
}
|
|
379
373
|
switch (msg.type) {
|
|
380
|
-
case
|
|
381
|
-
cb.onConnected()
|
|
382
|
-
setState(state,
|
|
383
|
-
return
|
|
384
|
-
case
|
|
385
|
-
const text = msg.text ??
|
|
386
|
-
if (!text) return
|
|
387
|
-
const isFinal = !!msg.isFinal
|
|
388
|
-
if (!isFinal) setState(state,
|
|
389
|
-
upsertUserPartial(state, text, isFinal)
|
|
390
|
-
cb.onTranscript(state.transcript)
|
|
391
|
-
return
|
|
374
|
+
case "connected":
|
|
375
|
+
cb.onConnected();
|
|
376
|
+
setState(state, "listening", cb);
|
|
377
|
+
return;
|
|
378
|
+
case "transcript": {
|
|
379
|
+
const text = msg.text ?? "";
|
|
380
|
+
if (!text) return;
|
|
381
|
+
const isFinal = !!msg.isFinal;
|
|
382
|
+
if (!isFinal) setState(state, "user_speaking", cb);
|
|
383
|
+
upsertUserPartial(state, text, isFinal);
|
|
384
|
+
cb.onTranscript(state.transcript);
|
|
385
|
+
return;
|
|
392
386
|
}
|
|
393
|
-
case
|
|
394
|
-
const id = `m${state.idCounter++}
|
|
395
|
-
state.agentBubbleId = id
|
|
396
|
-
state.transcript = [...state.transcript, { id, role:
|
|
397
|
-
cb.onTranscript(state.transcript)
|
|
398
|
-
const seq = typeof msg.seq ===
|
|
399
|
-
cb.onAgentTurnStart(seq)
|
|
400
|
-
setState(state,
|
|
401
|
-
return
|
|
387
|
+
case "agent_turn_start": {
|
|
388
|
+
const id = `m${state.idCounter++}`;
|
|
389
|
+
state.agentBubbleId = id;
|
|
390
|
+
state.transcript = [...state.transcript, { id, role: "agent", text: "" }];
|
|
391
|
+
cb.onTranscript(state.transcript);
|
|
392
|
+
const seq = typeof msg.seq === "number" ? msg.seq : void 0;
|
|
393
|
+
cb.onAgentTurnStart(seq);
|
|
394
|
+
setState(state, "agent_speaking", cb);
|
|
395
|
+
return;
|
|
402
396
|
}
|
|
403
|
-
case
|
|
404
|
-
const delta = msg.text ??
|
|
405
|
-
if (!delta || !state.agentBubbleId) return
|
|
406
|
-
const id = state.agentBubbleId
|
|
407
|
-
state.transcript = state.transcript.map(
|
|
408
|
-
e.id === id && e.role ===
|
|
409
|
-
)
|
|
410
|
-
cb.onTranscript(state.transcript)
|
|
411
|
-
return
|
|
397
|
+
case "agent_text": {
|
|
398
|
+
const delta = msg.text ?? "";
|
|
399
|
+
if (!delta || !state.agentBubbleId) return;
|
|
400
|
+
const id = state.agentBubbleId;
|
|
401
|
+
state.transcript = state.transcript.map(
|
|
402
|
+
(e) => e.id === id && e.role === "agent" ? { ...e, text: e.text + delta } : e
|
|
403
|
+
);
|
|
404
|
+
cb.onTranscript(state.transcript);
|
|
405
|
+
return;
|
|
412
406
|
}
|
|
413
|
-
case
|
|
414
|
-
state.agentBubbleId = null
|
|
415
|
-
const seq = typeof msg.seq ===
|
|
416
|
-
cb.onAgentTurnEnd(seq)
|
|
417
|
-
setState(state,
|
|
418
|
-
return
|
|
407
|
+
case "agent_turn_end": {
|
|
408
|
+
state.agentBubbleId = null;
|
|
409
|
+
const seq = typeof msg.seq === "number" ? msg.seq : void 0;
|
|
410
|
+
cb.onAgentTurnEnd(seq);
|
|
411
|
+
setState(state, "listening", cb);
|
|
412
|
+
return;
|
|
419
413
|
}
|
|
420
|
-
case
|
|
421
|
-
cb.onInterrupt()
|
|
422
|
-
return
|
|
423
|
-
case
|
|
424
|
-
const committed = (msg.committedText ??
|
|
414
|
+
case "interrupt":
|
|
415
|
+
cb.onInterrupt();
|
|
416
|
+
return;
|
|
417
|
+
case "agent_turn_abort": {
|
|
418
|
+
const committed = (msg.committedText ?? "").trim();
|
|
425
419
|
if (state.agentBubbleId) {
|
|
426
|
-
const id = state.agentBubbleId
|
|
420
|
+
const id = state.agentBubbleId;
|
|
427
421
|
if (committed) {
|
|
428
|
-
state.transcript = state.transcript.map(
|
|
429
|
-
e.id === id && e.role ===
|
|
430
|
-
)
|
|
422
|
+
state.transcript = state.transcript.map(
|
|
423
|
+
(e) => e.id === id && e.role === "agent" ? { ...e, text: committed, interrupted: true } : e
|
|
424
|
+
);
|
|
431
425
|
} else {
|
|
432
|
-
state.transcript = state.transcript.filter((e) => e.id !== id)
|
|
426
|
+
state.transcript = state.transcript.filter((e) => e.id !== id);
|
|
433
427
|
}
|
|
434
|
-
cb.onTranscript(state.transcript)
|
|
428
|
+
cb.onTranscript(state.transcript);
|
|
435
429
|
}
|
|
436
|
-
state.agentBubbleId = null
|
|
437
|
-
return
|
|
430
|
+
state.agentBubbleId = null;
|
|
431
|
+
return;
|
|
438
432
|
}
|
|
439
|
-
case
|
|
433
|
+
case "tool_call":
|
|
440
434
|
state.transcript = [
|
|
441
435
|
...state.transcript,
|
|
442
436
|
{
|
|
443
437
|
id: `m${state.idCounter++}`,
|
|
444
|
-
role:
|
|
445
|
-
text: `\u2192 ${String(msg.tool ??
|
|
446
|
-
}
|
|
447
|
-
]
|
|
448
|
-
cb.onTranscript(state.transcript)
|
|
449
|
-
return
|
|
450
|
-
case
|
|
438
|
+
role: "tool",
|
|
439
|
+
text: `\u2192 ${String(msg.tool ?? "?")}(${msg.args ? JSON.stringify(msg.args) : ""})`
|
|
440
|
+
}
|
|
441
|
+
];
|
|
442
|
+
cb.onTranscript(state.transcript);
|
|
443
|
+
return;
|
|
444
|
+
case "tool_result":
|
|
451
445
|
state.transcript = [
|
|
452
446
|
...state.transcript,
|
|
453
447
|
{
|
|
454
448
|
id: `m${state.idCounter++}`,
|
|
455
|
-
role:
|
|
456
|
-
text: `${msg.ok ?
|
|
457
|
-
}
|
|
458
|
-
]
|
|
459
|
-
cb.onTranscript(state.transcript)
|
|
460
|
-
return
|
|
461
|
-
case
|
|
462
|
-
const toolCallId = String(msg.toolCallId ??
|
|
463
|
-
const name = String(msg.name ??
|
|
464
|
-
const args = msg.args ?? {}
|
|
465
|
-
if (!toolCallId || !name) return
|
|
466
|
-
cb.onClientToolCall({ toolCallId, name, args })
|
|
467
|
-
return
|
|
449
|
+
role: "tool",
|
|
450
|
+
text: `${msg.ok ? "\u2713" : "\u2717"} ${String(msg.tool ?? "?")}`
|
|
451
|
+
}
|
|
452
|
+
];
|
|
453
|
+
cb.onTranscript(state.transcript);
|
|
454
|
+
return;
|
|
455
|
+
case "client_tool_call": {
|
|
456
|
+
const toolCallId = String(msg.toolCallId ?? "");
|
|
457
|
+
const name = String(msg.name ?? "");
|
|
458
|
+
const args = msg.args ?? {};
|
|
459
|
+
if (!toolCallId || !name) return;
|
|
460
|
+
cb.onClientToolCall({ toolCallId, name, args });
|
|
461
|
+
return;
|
|
468
462
|
}
|
|
469
|
-
case
|
|
470
|
-
const reasonRaw = String(msg.reason ??
|
|
471
|
-
const reason = mapEndReason(reasonRaw)
|
|
472
|
-
state.endReason = reason
|
|
463
|
+
case "call_end": {
|
|
464
|
+
const reasonRaw = String(msg.reason ?? "");
|
|
465
|
+
const reason = mapEndReason(reasonRaw);
|
|
466
|
+
state.endReason = reason;
|
|
473
467
|
state.transcript = [
|
|
474
468
|
...state.transcript,
|
|
475
469
|
{
|
|
476
470
|
id: `m${state.idCounter++}`,
|
|
477
|
-
role:
|
|
478
|
-
text: `call ended${reasonRaw ? ` (${reasonRaw})` :
|
|
479
|
-
}
|
|
480
|
-
]
|
|
481
|
-
cb.onTranscript(state.transcript)
|
|
482
|
-
cb.onCallEnd(reason)
|
|
483
|
-
return
|
|
471
|
+
role: "system",
|
|
472
|
+
text: `call ended${reasonRaw ? ` (${reasonRaw})` : ""}`
|
|
473
|
+
}
|
|
474
|
+
];
|
|
475
|
+
cb.onTranscript(state.transcript);
|
|
476
|
+
cb.onCallEnd(reason);
|
|
477
|
+
return;
|
|
484
478
|
}
|
|
485
|
-
case
|
|
486
|
-
const code = msg.code ??
|
|
487
|
-
const message = msg.message ??
|
|
488
|
-
cb.onError({ code, message })
|
|
489
|
-
return
|
|
479
|
+
case "error": {
|
|
480
|
+
const code = msg.code ?? "server_error";
|
|
481
|
+
const message = msg.message ?? "server error";
|
|
482
|
+
cb.onError({ code, message });
|
|
483
|
+
return;
|
|
490
484
|
}
|
|
491
485
|
}
|
|
492
486
|
}
|
|
493
487
|
var setState = (state, next, cb) => {
|
|
494
|
-
if (state.state === next) return
|
|
495
|
-
cb.onState(next)
|
|
496
|
-
}
|
|
488
|
+
if (state.state === next) return;
|
|
489
|
+
cb.onState(next);
|
|
490
|
+
};
|
|
497
491
|
var upsertUserPartial = (state, text, isFinal) => {
|
|
498
|
-
let idx = -1
|
|
492
|
+
let idx = -1;
|
|
499
493
|
for (let i = state.transcript.length - 1; i >= 0; i--) {
|
|
500
|
-
const e = state.transcript[i]
|
|
501
|
-
if (e.role ===
|
|
502
|
-
idx = i
|
|
503
|
-
break
|
|
494
|
+
const e = state.transcript[i];
|
|
495
|
+
if (e.role === "user" && e.committed === false) {
|
|
496
|
+
idx = i;
|
|
497
|
+
break;
|
|
504
498
|
}
|
|
505
499
|
}
|
|
506
500
|
if (idx === -1) {
|
|
507
501
|
state.transcript = [
|
|
508
502
|
...state.transcript,
|
|
509
|
-
{ id: `m${state.idCounter++}`, role:
|
|
510
|
-
]
|
|
511
|
-
return
|
|
512
|
-
}
|
|
513
|
-
const target = state.transcript[idx]
|
|
514
|
-
const next = [...state.transcript]
|
|
515
|
-
next[idx] = { ...target, text, committed: isFinal }
|
|
516
|
-
state.transcript = next
|
|
517
|
-
}
|
|
503
|
+
{ id: `m${state.idCounter++}`, role: "user", text, committed: isFinal }
|
|
504
|
+
];
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
const target = state.transcript[idx];
|
|
508
|
+
const next = [...state.transcript];
|
|
509
|
+
next[idx] = { ...target, text, committed: isFinal };
|
|
510
|
+
state.transcript = next;
|
|
511
|
+
};
|
|
518
512
|
function buildWsUrl(args) {
|
|
519
|
-
const base = new URL(args.apiBase)
|
|
520
|
-
const proto = base.protocol ===
|
|
521
|
-
const bargeQS = args.bargeIn === false ?
|
|
522
|
-
return `${proto}//${base.host}/v1/agents/${encodeURIComponent(args.agentId)}/call?token=${encodeURIComponent(args.token)}${bargeQS}
|
|
513
|
+
const base = new URL(args.apiBase);
|
|
514
|
+
const proto = base.protocol === "https:" ? "wss:" : "ws:";
|
|
515
|
+
const bargeQS = args.bargeIn === false ? "&barge=off" : "";
|
|
516
|
+
return `${proto}//${base.host}/v1/agents/${encodeURIComponent(args.agentId)}/call?token=${encodeURIComponent(args.token)}${bargeQS}`;
|
|
523
517
|
}
|
|
524
518
|
|
|
525
519
|
// src/clientTools.ts
|
|
526
|
-
var NAME_RE = /^[a-zA-Z_][a-zA-Z0-9_]
|
|
527
|
-
var MAX_TOOLS = 64
|
|
528
|
-
var MAX_USAGE = 500
|
|
529
|
-
var MAX_TIMEOUT_MS = 3e4
|
|
520
|
+
var NAME_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
521
|
+
var MAX_TOOLS = 64;
|
|
522
|
+
var MAX_USAGE = 500;
|
|
523
|
+
var MAX_TIMEOUT_MS = 3e4;
|
|
530
524
|
var validateClientToolMap = (tools) => {
|
|
531
|
-
if (tools === void 0) return
|
|
532
|
-
if (typeof tools !==
|
|
533
|
-
throw new Error(
|
|
525
|
+
if (tools === void 0) return;
|
|
526
|
+
if (typeof tools !== "object" || tools === null || Array.isArray(tools)) {
|
|
527
|
+
throw new Error("clientTools must be an object keyed by tool name");
|
|
534
528
|
}
|
|
535
|
-
const entries = Object.entries(tools)
|
|
529
|
+
const entries = Object.entries(tools);
|
|
536
530
|
if (entries.length > MAX_TOOLS) {
|
|
537
|
-
throw new Error(`clientTools may declare at most 64 tools (got ${entries.length})`)
|
|
531
|
+
throw new Error(`clientTools may declare at most 64 tools (got ${entries.length})`);
|
|
538
532
|
}
|
|
539
533
|
for (const [name, def] of entries) {
|
|
540
534
|
if (!NAME_RE.test(name)) {
|
|
541
535
|
throw new Error(
|
|
542
|
-
`clientTools["${name}"]: name must be a valid identifier (^[a-zA-Z_][a-zA-Z0-9_]*$)
|
|
543
|
-
)
|
|
536
|
+
`clientTools["${name}"]: name must be a valid identifier (^[a-zA-Z_][a-zA-Z0-9_]*$)`
|
|
537
|
+
);
|
|
544
538
|
}
|
|
545
|
-
if (!def || typeof def !==
|
|
546
|
-
throw new Error(`clientTools["${name}"]: must be an object`)
|
|
539
|
+
if (!def || typeof def !== "object") {
|
|
540
|
+
throw new Error(`clientTools["${name}"]: must be an object`);
|
|
547
541
|
}
|
|
548
|
-
if (typeof def.description !==
|
|
549
|
-
throw new Error(`clientTools["${name}"]: must have a description`)
|
|
542
|
+
if (typeof def.description !== "string" || def.description.length === 0) {
|
|
543
|
+
throw new Error(`clientTools["${name}"]: must have a description`);
|
|
550
544
|
}
|
|
551
|
-
if (typeof def.handler !==
|
|
552
|
-
throw new Error(`clientTools["${name}"]: must have a handler function`)
|
|
545
|
+
if (typeof def.handler !== "function") {
|
|
546
|
+
throw new Error(`clientTools["${name}"]: must have a handler function`);
|
|
553
547
|
}
|
|
554
548
|
if (def.usage !== void 0 && def.usage.length > MAX_USAGE) {
|
|
555
|
-
throw new Error(`clientTools["${name}"]: usage must be \u2264500 chars`)
|
|
549
|
+
throw new Error(`clientTools["${name}"]: usage must be \u2264500 chars`);
|
|
556
550
|
}
|
|
557
|
-
if (
|
|
558
|
-
|
|
559
|
-
(!Number.isFinite(def.timeoutMs) || def.timeoutMs <= 0 || def.timeoutMs > MAX_TIMEOUT_MS)
|
|
560
|
-
) {
|
|
561
|
-
throw new Error(`clientTools["${name}"]: timeoutMs must be in (0, 30000]`)
|
|
551
|
+
if (def.timeoutMs !== void 0 && (!Number.isFinite(def.timeoutMs) || def.timeoutMs <= 0 || def.timeoutMs > MAX_TIMEOUT_MS)) {
|
|
552
|
+
throw new Error(`clientTools["${name}"]: timeoutMs must be in (0, 30000]`);
|
|
562
553
|
}
|
|
563
554
|
}
|
|
564
|
-
}
|
|
555
|
+
};
|
|
565
556
|
var buildRegisterFrame = (tools) => ({
|
|
566
|
-
type:
|
|
557
|
+
type: "client_tools_register",
|
|
567
558
|
tools: Object.entries(tools).map(([name, def]) => ({
|
|
568
559
|
name,
|
|
569
560
|
description: def.description,
|
|
570
561
|
parameters: def.parameters,
|
|
571
|
-
...
|
|
572
|
-
...
|
|
573
|
-
}))
|
|
574
|
-
})
|
|
562
|
+
...def.usage !== void 0 ? { usage: def.usage } : {},
|
|
563
|
+
...def.timeoutMs !== void 0 ? { timeoutMs: def.timeoutMs } : {}
|
|
564
|
+
}))
|
|
565
|
+
});
|
|
575
566
|
var dispatchClientToolCall = (send, tools, frame) => {
|
|
576
567
|
const safeSend = (payload) => {
|
|
577
568
|
try {
|
|
578
|
-
send(payload)
|
|
579
|
-
} catch {
|
|
580
|
-
|
|
581
|
-
|
|
569
|
+
send(payload);
|
|
570
|
+
} catch {
|
|
571
|
+
}
|
|
572
|
+
};
|
|
573
|
+
const tool = tools[frame.name];
|
|
582
574
|
if (!tool) {
|
|
583
575
|
safeSend({
|
|
584
|
-
type:
|
|
576
|
+
type: "client_tool_result",
|
|
585
577
|
toolCallId: frame.toolCallId,
|
|
586
|
-
error: `No handler for ${frame.name}
|
|
587
|
-
})
|
|
588
|
-
return
|
|
578
|
+
error: `No handler for ${frame.name}`
|
|
579
|
+
});
|
|
580
|
+
return;
|
|
589
581
|
}
|
|
590
582
|
void (async () => {
|
|
591
583
|
try {
|
|
592
|
-
const out = await tool.handler(frame.args)
|
|
584
|
+
const out = await tool.handler(frame.args);
|
|
593
585
|
safeSend({
|
|
594
|
-
type:
|
|
586
|
+
type: "client_tool_result",
|
|
595
587
|
toolCallId: frame.toolCallId,
|
|
596
|
-
result: typeof out ===
|
|
597
|
-
})
|
|
588
|
+
result: typeof out === "string" ? out : JSON.stringify(out)
|
|
589
|
+
});
|
|
598
590
|
} catch (err) {
|
|
599
591
|
safeSend({
|
|
600
|
-
type:
|
|
592
|
+
type: "client_tool_result",
|
|
601
593
|
toolCallId: frame.toolCallId,
|
|
602
|
-
error: err instanceof Error ? err.message : String(err)
|
|
603
|
-
})
|
|
594
|
+
error: err instanceof Error ? err.message : String(err)
|
|
595
|
+
});
|
|
604
596
|
}
|
|
605
|
-
})()
|
|
606
|
-
}
|
|
597
|
+
})();
|
|
598
|
+
};
|
|
607
599
|
|
|
608
600
|
// src/ClientMarksBuffer.ts
|
|
609
601
|
var createClientMarksBuffer = (args) => {
|
|
610
|
-
const now = args.now ?? (() => performance.now())
|
|
611
|
-
let pendingFirstOutboundAt = null
|
|
612
|
-
const inFlight = /* @__PURE__ */ new Map()
|
|
602
|
+
const now = args.now ?? (() => performance.now());
|
|
603
|
+
let pendingFirstOutboundAt = null;
|
|
604
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
613
605
|
const tryEmit = (seq) => {
|
|
614
|
-
const slot = inFlight.get(seq)
|
|
615
|
-
if (!slot) return
|
|
616
|
-
if (!slot.ended) return
|
|
617
|
-
const marks = {}
|
|
606
|
+
const slot = inFlight.get(seq);
|
|
607
|
+
if (!slot) return;
|
|
608
|
+
if (!slot.ended) return;
|
|
609
|
+
const marks = {};
|
|
618
610
|
if (slot.firstOutboundAt !== null && slot.firstAudibleAt !== null) {
|
|
619
|
-
marks.client_mic_to_first_audible_ms = slot.firstAudibleAt - slot.firstOutboundAt
|
|
611
|
+
marks.client_mic_to_first_audible_ms = slot.firstAudibleAt - slot.firstOutboundAt;
|
|
620
612
|
}
|
|
621
613
|
args.send({
|
|
622
|
-
type:
|
|
614
|
+
type: "client_marks",
|
|
623
615
|
seq,
|
|
624
616
|
marks,
|
|
625
|
-
clientNow: Date.now()
|
|
626
|
-
})
|
|
627
|
-
inFlight.delete(seq)
|
|
628
|
-
}
|
|
617
|
+
clientNow: Date.now()
|
|
618
|
+
});
|
|
619
|
+
inFlight.delete(seq);
|
|
620
|
+
};
|
|
629
621
|
const markFirstOutboundAudio = () => {
|
|
630
|
-
if (pendingFirstOutboundAt !== null) return
|
|
631
|
-
pendingFirstOutboundAt = now()
|
|
632
|
-
}
|
|
622
|
+
if (pendingFirstOutboundAt !== null) return;
|
|
623
|
+
pendingFirstOutboundAt = now();
|
|
624
|
+
};
|
|
633
625
|
const markFirstAudibleOutput = () => {
|
|
634
|
-
let target
|
|
626
|
+
let target;
|
|
635
627
|
for (const slot of inFlight.values()) {
|
|
636
628
|
if (!slot.ended) {
|
|
637
|
-
target = slot
|
|
629
|
+
target = slot;
|
|
638
630
|
}
|
|
639
631
|
}
|
|
640
|
-
if (!target) return
|
|
641
|
-
if (target.firstAudibleAt !== null) return
|
|
642
|
-
target.firstAudibleAt = now()
|
|
643
|
-
}
|
|
632
|
+
if (!target) return;
|
|
633
|
+
if (target.firstAudibleAt !== null) return;
|
|
634
|
+
target.firstAudibleAt = now();
|
|
635
|
+
};
|
|
644
636
|
const onAgentTurnStart = (seq) => {
|
|
645
637
|
inFlight.set(seq, {
|
|
646
638
|
firstOutboundAt: pendingFirstOutboundAt,
|
|
647
639
|
firstAudibleAt: null,
|
|
648
|
-
ended: false
|
|
649
|
-
})
|
|
650
|
-
pendingFirstOutboundAt = null
|
|
651
|
-
}
|
|
640
|
+
ended: false
|
|
641
|
+
});
|
|
642
|
+
pendingFirstOutboundAt = null;
|
|
643
|
+
};
|
|
652
644
|
const onAgentTurnEnd = (seq) => {
|
|
653
|
-
const slot = inFlight.get(seq)
|
|
645
|
+
const slot = inFlight.get(seq);
|
|
654
646
|
if (!slot) {
|
|
655
|
-
args.send({ type:
|
|
656
|
-
return
|
|
647
|
+
args.send({ type: "client_marks", seq, marks: {}, clientNow: Date.now() });
|
|
648
|
+
return;
|
|
657
649
|
}
|
|
658
|
-
slot.ended = true
|
|
659
|
-
tryEmit(seq)
|
|
660
|
-
}
|
|
650
|
+
slot.ended = true;
|
|
651
|
+
tryEmit(seq);
|
|
652
|
+
};
|
|
661
653
|
const flush = () => {
|
|
662
654
|
for (const seq of [...inFlight.keys()]) {
|
|
663
|
-
const slot = inFlight.get(seq)
|
|
664
|
-
slot.ended = true
|
|
665
|
-
tryEmit(seq)
|
|
655
|
+
const slot = inFlight.get(seq);
|
|
656
|
+
slot.ended = true;
|
|
657
|
+
tryEmit(seq);
|
|
666
658
|
}
|
|
667
|
-
pendingFirstOutboundAt = null
|
|
668
|
-
}
|
|
659
|
+
pendingFirstOutboundAt = null;
|
|
660
|
+
};
|
|
669
661
|
return {
|
|
670
662
|
markFirstOutboundAudio,
|
|
671
663
|
markFirstAudibleOutput,
|
|
672
664
|
onAgentTurnStart,
|
|
673
665
|
onAgentTurnEnd,
|
|
674
|
-
flush
|
|
675
|
-
}
|
|
676
|
-
}
|
|
666
|
+
flush
|
|
667
|
+
};
|
|
668
|
+
};
|
|
677
669
|
|
|
678
670
|
// src/VoiceClient.ts
|
|
679
671
|
var BrowserVoiceClient = class {
|
|
680
672
|
constructor(args) {
|
|
681
|
-
this.rws = null
|
|
682
|
-
this.capture = null
|
|
683
|
-
this.playback = null
|
|
684
|
-
this.muted = false
|
|
685
|
-
this.inputVolume = 0
|
|
686
|
-
this.outputVolume = 0
|
|
687
|
-
this.startedAt = null
|
|
688
|
-
this.endedFired = false
|
|
689
|
-
this.lastError = null
|
|
673
|
+
this.rws = null;
|
|
674
|
+
this.capture = null;
|
|
675
|
+
this.playback = null;
|
|
676
|
+
this.muted = false;
|
|
677
|
+
this.inputVolume = 0;
|
|
678
|
+
this.outputVolume = 0;
|
|
679
|
+
this.startedAt = null;
|
|
680
|
+
this.endedFired = false;
|
|
681
|
+
this.lastError = null;
|
|
690
682
|
this.end = () => {
|
|
691
|
-
this.teardown(
|
|
692
|
-
}
|
|
683
|
+
this.teardown("user_hangup");
|
|
684
|
+
};
|
|
693
685
|
this.mute = () => {
|
|
694
|
-
if (this.muted) return
|
|
695
|
-
this.muted = true
|
|
696
|
-
this.capture?.mute(true)
|
|
697
|
-
}
|
|
686
|
+
if (this.muted) return;
|
|
687
|
+
this.muted = true;
|
|
688
|
+
this.capture?.mute(true);
|
|
689
|
+
};
|
|
698
690
|
this.unmute = () => {
|
|
699
|
-
if (!this.muted) return
|
|
700
|
-
this.muted = false
|
|
701
|
-
this.capture?.mute(false)
|
|
702
|
-
}
|
|
691
|
+
if (!this.muted) return;
|
|
692
|
+
this.muted = false;
|
|
693
|
+
this.capture?.mute(false);
|
|
694
|
+
};
|
|
703
695
|
// ---------------------------------------------------------------
|
|
704
696
|
// Internal
|
|
705
697
|
// ---------------------------------------------------------------
|
|
706
698
|
this.sendClientToolsRegister = () => {
|
|
707
|
-
const frame = buildRegisterFrame(this.args.options.clientTools ?? {})
|
|
708
|
-
this.rws?.send(JSON.stringify(frame))
|
|
709
|
-
}
|
|
699
|
+
const frame = buildRegisterFrame(this.args.options.clientTools ?? {});
|
|
700
|
+
this.rws?.send(JSON.stringify(frame));
|
|
701
|
+
};
|
|
710
702
|
this.setState = (next) => {
|
|
711
|
-
if (this.proto.state === next) return
|
|
712
|
-
this.proto.state = next
|
|
713
|
-
this.args.options.onStateChange?.(next)
|
|
714
|
-
}
|
|
703
|
+
if (this.proto.state === next) return;
|
|
704
|
+
this.proto.state = next;
|
|
705
|
+
this.args.options.onStateChange?.(next);
|
|
706
|
+
};
|
|
715
707
|
this.emitError = (err) => {
|
|
716
|
-
this.lastError = err
|
|
717
|
-
this.args.options.onError?.(err)
|
|
718
|
-
}
|
|
708
|
+
this.lastError = err;
|
|
709
|
+
this.args.options.onError?.(err);
|
|
710
|
+
};
|
|
719
711
|
this.handleSocketEvent = (ev) => {
|
|
720
712
|
switch (ev.type) {
|
|
721
|
-
case
|
|
722
|
-
void this.startCapture()
|
|
723
|
-
break
|
|
724
|
-
case
|
|
725
|
-
this.proto.transcript = []
|
|
726
|
-
this.proto.agentBubbleId = null
|
|
727
|
-
this.args.options.onTranscript?.(this.proto.transcript)
|
|
728
|
-
void this.startCapture()
|
|
729
|
-
this.setState(
|
|
730
|
-
break
|
|
731
|
-
case
|
|
732
|
-
if (typeof ev.data ===
|
|
713
|
+
case "open":
|
|
714
|
+
void this.startCapture();
|
|
715
|
+
break;
|
|
716
|
+
case "reconnected":
|
|
717
|
+
this.proto.transcript = [];
|
|
718
|
+
this.proto.agentBubbleId = null;
|
|
719
|
+
this.args.options.onTranscript?.(this.proto.transcript);
|
|
720
|
+
void this.startCapture();
|
|
721
|
+
this.setState("listening");
|
|
722
|
+
break;
|
|
723
|
+
case "message":
|
|
724
|
+
if (typeof ev.data === "string") {
|
|
733
725
|
handleServerMessage(ev.data, this.proto, {
|
|
734
726
|
onState: this.setState,
|
|
735
727
|
onTranscript: (entries) => this.args.options.onTranscript?.(entries),
|
|
736
728
|
onError: this.emitError,
|
|
737
729
|
onInterrupt: () => {
|
|
738
|
-
this.playback?.flush()
|
|
739
|
-
this.args.options.onInterrupt?.()
|
|
730
|
+
this.playback?.flush();
|
|
731
|
+
this.args.options.onInterrupt?.();
|
|
740
732
|
},
|
|
741
733
|
onAgentTurnStart: (seq) => {
|
|
742
|
-
if (typeof seq ===
|
|
743
|
-
this.args.options.onAgentTurnStart?.()
|
|
734
|
+
if (typeof seq === "number") this.marks.onAgentTurnStart(seq);
|
|
735
|
+
this.args.options.onAgentTurnStart?.();
|
|
744
736
|
},
|
|
745
737
|
onAgentTurnEnd: (seq) => {
|
|
746
|
-
if (typeof seq ===
|
|
738
|
+
if (typeof seq === "number") this.marks.onAgentTurnEnd(seq);
|
|
747
739
|
},
|
|
748
740
|
onCallEnd: (reason) => this.teardown(reason),
|
|
749
741
|
onConnected: () => this.sendClientToolsRegister(),
|
|
750
|
-
onClientToolCall: (frame) =>
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
})
|
|
742
|
+
onClientToolCall: (frame) => dispatchClientToolCall(
|
|
743
|
+
(f) => this.rws?.send(JSON.stringify(f)),
|
|
744
|
+
this.args.options.clientTools ?? {},
|
|
745
|
+
frame
|
|
746
|
+
)
|
|
747
|
+
});
|
|
757
748
|
} else {
|
|
758
|
-
this.marks.markFirstAudibleOutput()
|
|
759
|
-
this.playback?.enqueue(ev.data)
|
|
749
|
+
this.marks.markFirstAudibleOutput();
|
|
750
|
+
this.playback?.enqueue(ev.data);
|
|
760
751
|
}
|
|
761
|
-
break
|
|
762
|
-
case
|
|
752
|
+
break;
|
|
753
|
+
case "close":
|
|
763
754
|
if (ev.permanent) {
|
|
764
|
-
const reason = this.proto.endReason ?? (this.lastError ?
|
|
765
|
-
this.teardown(reason)
|
|
755
|
+
const reason = this.proto.endReason ?? (this.lastError ? "error" : "user_hangup");
|
|
756
|
+
this.teardown(reason);
|
|
766
757
|
}
|
|
767
|
-
break
|
|
768
|
-
case
|
|
769
|
-
this.emitError({ code:
|
|
770
|
-
break
|
|
758
|
+
break;
|
|
759
|
+
case "error":
|
|
760
|
+
this.emitError({ code: "socket_error", message: ev.error.message });
|
|
761
|
+
break;
|
|
771
762
|
}
|
|
772
|
-
}
|
|
763
|
+
};
|
|
773
764
|
this.startCapture = async () => {
|
|
774
|
-
if (this.capture?.isCapturing()) return
|
|
765
|
+
if (this.capture?.isCapturing()) return;
|
|
775
766
|
this.capture = createAudioCapture({
|
|
776
767
|
onChunk: (pcm) => {
|
|
777
|
-
this.marks.markFirstOutboundAudio()
|
|
778
|
-
this.rws?.send(pcm)
|
|
768
|
+
this.marks.markFirstOutboundAudio();
|
|
769
|
+
this.rws?.send(pcm);
|
|
779
770
|
},
|
|
780
771
|
onVolume: (v) => {
|
|
781
|
-
this.inputVolume = v
|
|
782
|
-
this.args.options.onVolume?.({ input: v, output: this.outputVolume })
|
|
772
|
+
this.inputVolume = v;
|
|
773
|
+
this.args.options.onVolume?.({ input: v, output: this.outputVolume });
|
|
783
774
|
},
|
|
784
775
|
onError: (err) => {
|
|
785
776
|
this.emitError({
|
|
786
|
-
code: err.name ===
|
|
787
|
-
message: err.message
|
|
788
|
-
})
|
|
789
|
-
}
|
|
790
|
-
})
|
|
791
|
-
if (this.muted) this.capture.mute(true)
|
|
777
|
+
code: err.name === "NotAllowedError" ? "mic_denied" : "mic_start_failed",
|
|
778
|
+
message: err.message
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
});
|
|
782
|
+
if (this.muted) this.capture.mute(true);
|
|
792
783
|
try {
|
|
793
|
-
await this.capture.start()
|
|
794
|
-
} catch {
|
|
795
|
-
|
|
784
|
+
await this.capture.start();
|
|
785
|
+
} catch {
|
|
786
|
+
}
|
|
787
|
+
};
|
|
796
788
|
this.teardown = (reason) => {
|
|
797
789
|
try {
|
|
798
|
-
this.marks.flush()
|
|
799
|
-
} catch {
|
|
800
|
-
|
|
801
|
-
this.capture
|
|
802
|
-
this.
|
|
803
|
-
this.playback
|
|
790
|
+
this.marks.flush();
|
|
791
|
+
} catch {
|
|
792
|
+
}
|
|
793
|
+
this.capture?.stop();
|
|
794
|
+
this.capture = null;
|
|
795
|
+
this.playback?.close();
|
|
796
|
+
this.playback = null;
|
|
804
797
|
try {
|
|
805
|
-
this.rws?.close(1e3, reason)
|
|
806
|
-
} catch {
|
|
807
|
-
|
|
808
|
-
this.
|
|
809
|
-
this.
|
|
810
|
-
|
|
798
|
+
this.rws?.close(1e3, reason);
|
|
799
|
+
} catch {
|
|
800
|
+
}
|
|
801
|
+
this.rws = null;
|
|
802
|
+
this.setState("ended");
|
|
803
|
+
this.fireEndOnce(reason);
|
|
804
|
+
};
|
|
811
805
|
this.fireEndOnce = (reason) => {
|
|
812
|
-
if (this.endedFired) return
|
|
813
|
-
this.endedFired = true
|
|
814
|
-
const startedAt = this.startedAt ?? Date.now()
|
|
806
|
+
if (this.endedFired) return;
|
|
807
|
+
this.endedFired = true;
|
|
808
|
+
const startedAt = this.startedAt ?? Date.now();
|
|
815
809
|
this.args.options.onEnd?.({
|
|
816
810
|
reason,
|
|
817
|
-
errorCode: reason ===
|
|
818
|
-
durationMs: Date.now() - startedAt
|
|
819
|
-
})
|
|
820
|
-
}
|
|
821
|
-
this.args = args
|
|
822
|
-
this.proto = createProtocolState()
|
|
823
|
-
validateClientToolMap(args.options.clientTools)
|
|
811
|
+
errorCode: reason === "error" ? this.lastError?.code : void 0,
|
|
812
|
+
durationMs: Date.now() - startedAt
|
|
813
|
+
});
|
|
814
|
+
};
|
|
815
|
+
this.args = args;
|
|
816
|
+
this.proto = createProtocolState();
|
|
817
|
+
validateClientToolMap(args.options.clientTools);
|
|
824
818
|
this.marks = createClientMarksBuffer({
|
|
825
819
|
send: (frame) => {
|
|
826
820
|
try {
|
|
827
|
-
this.rws?.send(JSON.stringify(frame))
|
|
828
|
-
} catch {
|
|
829
|
-
|
|
830
|
-
|
|
821
|
+
this.rws?.send(JSON.stringify(frame));
|
|
822
|
+
} catch {
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
});
|
|
831
826
|
}
|
|
832
827
|
// ---------------------------------------------------------------
|
|
833
828
|
// Call interface
|
|
834
829
|
// ---------------------------------------------------------------
|
|
835
830
|
get state() {
|
|
836
|
-
return this.proto.state
|
|
831
|
+
return this.proto.state;
|
|
837
832
|
}
|
|
838
833
|
get transcript() {
|
|
839
|
-
return this.proto.transcript.slice()
|
|
834
|
+
return this.proto.transcript.slice();
|
|
840
835
|
}
|
|
841
836
|
get isMuted() {
|
|
842
|
-
return this.muted
|
|
837
|
+
return this.muted;
|
|
843
838
|
}
|
|
844
839
|
// ---------------------------------------------------------------
|
|
845
840
|
// Lifecycle — called by the factory immediately after construction.
|
|
@@ -847,52 +842,54 @@ var BrowserVoiceClient = class {
|
|
|
847
842
|
// failures arrive via `onError`.
|
|
848
843
|
// ---------------------------------------------------------------
|
|
849
844
|
async start() {
|
|
850
|
-
this.setState(
|
|
851
|
-
this.startedAt = Date.now()
|
|
845
|
+
this.setState("connecting");
|
|
846
|
+
this.startedAt = Date.now();
|
|
852
847
|
const url = buildWsUrl({
|
|
853
848
|
apiBase: this.args.config.apiBase,
|
|
854
849
|
agentId: this.args.options.agentId,
|
|
855
850
|
token: this.args.token,
|
|
856
|
-
bargeIn: this.args.options.bargeIn
|
|
857
|
-
})
|
|
851
|
+
bargeIn: this.args.options.bargeIn
|
|
852
|
+
});
|
|
858
853
|
this.playback = createAudioPlayback({
|
|
859
854
|
onVolume: (v) => {
|
|
860
|
-
this.outputVolume = v
|
|
861
|
-
this.args.options.onVolume?.({ input: this.inputVolume, output: v })
|
|
862
|
-
}
|
|
863
|
-
})
|
|
855
|
+
this.outputVolume = v;
|
|
856
|
+
this.args.options.onVolume?.({ input: this.inputVolume, output: v });
|
|
857
|
+
}
|
|
858
|
+
});
|
|
864
859
|
try {
|
|
865
|
-
await this.playback.resume()
|
|
866
|
-
} catch {
|
|
860
|
+
await this.playback.resume();
|
|
861
|
+
} catch {
|
|
862
|
+
}
|
|
867
863
|
this.rws = createReconnectingWebSocket(
|
|
868
864
|
{
|
|
869
865
|
url,
|
|
870
866
|
wsFactory: this.args.wsFactory,
|
|
871
|
-
maxRetries: 3
|
|
867
|
+
maxRetries: 3
|
|
872
868
|
},
|
|
873
|
-
(ev) => this.handleSocketEvent(ev)
|
|
874
|
-
)
|
|
869
|
+
(ev) => this.handleSocketEvent(ev)
|
|
870
|
+
);
|
|
875
871
|
}
|
|
876
|
-
}
|
|
872
|
+
};
|
|
877
873
|
|
|
878
874
|
// src/webrtc/createWebRtcCall.ts
|
|
879
875
|
async function createWebRtcCall(opts) {
|
|
880
|
-
validateClientToolMap(opts.clientTools)
|
|
881
|
-
const proto = createProtocolState()
|
|
882
|
-
let muted = false
|
|
883
|
-
let ended = false
|
|
884
|
-
const tools = opts.clientTools ?? {}
|
|
876
|
+
validateClientToolMap(opts.clientTools);
|
|
877
|
+
const proto = createProtocolState();
|
|
878
|
+
let muted = false;
|
|
879
|
+
let ended = false;
|
|
880
|
+
const tools = opts.clientTools ?? {};
|
|
885
881
|
const sendControl = (frame) => {
|
|
886
|
-
if (dc?.readyState !==
|
|
882
|
+
if (dc?.readyState !== "open") return;
|
|
887
883
|
try {
|
|
888
|
-
dc.send(JSON.stringify(frame))
|
|
889
|
-
} catch {
|
|
890
|
-
|
|
884
|
+
dc.send(JSON.stringify(frame));
|
|
885
|
+
} catch {
|
|
886
|
+
}
|
|
887
|
+
};
|
|
891
888
|
const fireState = (next) => {
|
|
892
|
-
if (proto.state === next) return
|
|
893
|
-
proto.state = next
|
|
894
|
-
opts.onStateChange?.(next)
|
|
895
|
-
}
|
|
889
|
+
if (proto.state === next) return;
|
|
890
|
+
proto.state = next;
|
|
891
|
+
opts.onStateChange?.(next);
|
|
892
|
+
};
|
|
896
893
|
const dispatch = (raw) => {
|
|
897
894
|
handleServerMessage(raw, proto, {
|
|
898
895
|
onState: fireState,
|
|
@@ -900,383 +897,466 @@ async function createWebRtcCall(opts) {
|
|
|
900
897
|
onError: (err) => opts.onError?.(err),
|
|
901
898
|
onInterrupt: () => opts.onInterrupt?.(),
|
|
902
899
|
onAgentTurnStart: () => opts.onAgentTurnStart?.(),
|
|
903
|
-
onAgentTurnEnd: () => {
|
|
900
|
+
onAgentTurnEnd: () => {
|
|
901
|
+
},
|
|
904
902
|
onCallEnd: () => teardown(),
|
|
905
903
|
onConnected: () => {
|
|
906
904
|
if (Object.keys(tools).length > 0) {
|
|
907
|
-
sendControl(buildRegisterFrame(tools))
|
|
905
|
+
sendControl(buildRegisterFrame(tools));
|
|
908
906
|
}
|
|
909
907
|
},
|
|
910
908
|
onClientToolCall: (frame) => {
|
|
911
|
-
dispatchClientToolCall(sendControl, tools, frame)
|
|
912
|
-
}
|
|
913
|
-
})
|
|
914
|
-
}
|
|
915
|
-
fireState(
|
|
909
|
+
dispatchClientToolCall(sendControl, tools, frame);
|
|
910
|
+
}
|
|
911
|
+
});
|
|
912
|
+
};
|
|
913
|
+
fireState("connecting");
|
|
916
914
|
const pc = new RTCPeerConnection({
|
|
917
|
-
iceServers: [{ urls:
|
|
918
|
-
})
|
|
919
|
-
const audioEl = document.createElement(
|
|
920
|
-
audioEl.autoplay = true
|
|
921
|
-
audioEl.style.display =
|
|
922
|
-
document.body.appendChild(audioEl)
|
|
915
|
+
iceServers: [{ urls: "stun:stun.l.google.com:19302" }]
|
|
916
|
+
});
|
|
917
|
+
const audioEl = document.createElement("audio");
|
|
918
|
+
audioEl.autoplay = true;
|
|
919
|
+
audioEl.style.display = "none";
|
|
920
|
+
document.body.appendChild(audioEl);
|
|
923
921
|
pc.ontrack = (event) => {
|
|
924
|
-
audioEl.srcObject = event.streams[0] ?? new MediaStream([event.track])
|
|
925
|
-
}
|
|
926
|
-
let mic
|
|
922
|
+
audioEl.srcObject = event.streams[0] ?? new MediaStream([event.track]);
|
|
923
|
+
};
|
|
924
|
+
let mic;
|
|
927
925
|
try {
|
|
928
|
-
mic = await navigator.mediaDevices.getUserMedia({ audio: true })
|
|
926
|
+
mic = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
929
927
|
} catch (err) {
|
|
930
|
-
const code =
|
|
931
|
-
err instanceof DOMException && err.name === 'NotAllowedError'
|
|
932
|
-
? 'mic_denied'
|
|
933
|
-
: 'mic_start_failed'
|
|
928
|
+
const code = err instanceof DOMException && err.name === "NotAllowedError" ? "mic_denied" : "mic_start_failed";
|
|
934
929
|
opts.onError?.({
|
|
935
930
|
code,
|
|
936
|
-
message: err instanceof Error ? err.message :
|
|
937
|
-
})
|
|
938
|
-
fireState(
|
|
939
|
-
pc.close()
|
|
940
|
-
audioEl.remove()
|
|
941
|
-
throw err
|
|
942
|
-
}
|
|
943
|
-
for (const track of mic.getAudioTracks()) pc.addTrack(track, mic)
|
|
944
|
-
const dc = pc.createDataChannel(
|
|
931
|
+
message: err instanceof Error ? err.message : "getUserMedia failed"
|
|
932
|
+
});
|
|
933
|
+
fireState("error");
|
|
934
|
+
pc.close();
|
|
935
|
+
audioEl.remove();
|
|
936
|
+
throw err;
|
|
937
|
+
}
|
|
938
|
+
for (const track of mic.getAudioTracks()) pc.addTrack(track, mic);
|
|
939
|
+
const dc = pc.createDataChannel("control", { ordered: true });
|
|
945
940
|
dc.onmessage = (e) => {
|
|
946
|
-
if (typeof e.data ===
|
|
947
|
-
}
|
|
941
|
+
if (typeof e.data === "string") dispatch(e.data);
|
|
942
|
+
};
|
|
948
943
|
dc.onerror = () => {
|
|
949
|
-
opts.onError?.({ code:
|
|
950
|
-
}
|
|
944
|
+
opts.onError?.({ code: "socket_error", message: "control channel error" });
|
|
945
|
+
};
|
|
951
946
|
dc.onopen = () => {
|
|
952
947
|
if (Object.keys(tools).length > 0) {
|
|
953
|
-
sendControl(buildRegisterFrame(tools))
|
|
948
|
+
sendControl(buildRegisterFrame(tools));
|
|
954
949
|
}
|
|
955
|
-
}
|
|
956
|
-
const gateway = opts.webrtcGatewayBase ||
|
|
957
|
-
const offerUrl = gateway
|
|
958
|
-
|
|
959
|
-
: `${opts.apiBase}/v1/agents/${encodeURIComponent(opts.agentId)}/webrtc/offer?token=${encodeURIComponent(opts.token)}`
|
|
960
|
-
const iceUrl = gateway
|
|
961
|
-
? `${gateway}/webrtc/ice?token=${encodeURIComponent(opts.token)}`
|
|
962
|
-
: `${opts.apiBase}/v1/agents/${encodeURIComponent(opts.agentId)}/webrtc/ice?token=${encodeURIComponent(opts.token)}`
|
|
950
|
+
};
|
|
951
|
+
const gateway = opts.webrtcGatewayBase || "";
|
|
952
|
+
const offerUrl = gateway ? `${gateway}/webrtc/offer?token=${encodeURIComponent(opts.token)}` : `${opts.apiBase}/v1/agents/${encodeURIComponent(opts.agentId)}/webrtc/offer?token=${encodeURIComponent(opts.token)}`;
|
|
953
|
+
const iceUrl = gateway ? `${gateway}/webrtc/ice?token=${encodeURIComponent(opts.token)}` : `${opts.apiBase}/v1/agents/${encodeURIComponent(opts.agentId)}/webrtc/ice?token=${encodeURIComponent(opts.token)}`;
|
|
963
954
|
const teardown = () => {
|
|
964
|
-
if (ended) return
|
|
965
|
-
ended = true
|
|
955
|
+
if (ended) return;
|
|
956
|
+
ended = true;
|
|
966
957
|
try {
|
|
967
|
-
mic.getTracks().forEach((t) => t.stop())
|
|
968
|
-
} catch {
|
|
958
|
+
mic.getTracks().forEach((t) => t.stop());
|
|
959
|
+
} catch {
|
|
960
|
+
}
|
|
969
961
|
try {
|
|
970
|
-
pc.close()
|
|
971
|
-
} catch {
|
|
962
|
+
pc.close();
|
|
963
|
+
} catch {
|
|
964
|
+
}
|
|
972
965
|
try {
|
|
973
|
-
audioEl.remove()
|
|
974
|
-
} catch {
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
966
|
+
audioEl.remove();
|
|
967
|
+
} catch {
|
|
968
|
+
}
|
|
969
|
+
fireState("ended");
|
|
970
|
+
opts.onEnd?.();
|
|
971
|
+
};
|
|
972
|
+
let callId = null;
|
|
973
|
+
const pendingCandidates = [];
|
|
980
974
|
const postCandidate = (candidate) => {
|
|
981
975
|
void fetch(iceUrl, {
|
|
982
|
-
method:
|
|
983
|
-
headers: {
|
|
984
|
-
body: JSON.stringify({ callId, candidate })
|
|
985
|
-
}).catch(() => {
|
|
986
|
-
|
|
976
|
+
method: "POST",
|
|
977
|
+
headers: { "content-type": "application/json" },
|
|
978
|
+
body: JSON.stringify({ callId, candidate })
|
|
979
|
+
}).catch(() => {
|
|
980
|
+
});
|
|
981
|
+
};
|
|
987
982
|
pc.onicecandidate = (e) => {
|
|
988
|
-
if (!e.candidate) return
|
|
989
|
-
if (callId) postCandidate(e.candidate)
|
|
990
|
-
else pendingCandidates.push(e.candidate)
|
|
991
|
-
}
|
|
983
|
+
if (!e.candidate) return;
|
|
984
|
+
if (callId) postCandidate(e.candidate);
|
|
985
|
+
else pendingCandidates.push(e.candidate);
|
|
986
|
+
};
|
|
992
987
|
pc.onconnectionstatechange = () => {
|
|
993
|
-
const s = pc.connectionState
|
|
994
|
-
if (s ===
|
|
995
|
-
if (s ===
|
|
996
|
-
opts.onError?.({ code:
|
|
997
|
-
teardown()
|
|
988
|
+
const s = pc.connectionState;
|
|
989
|
+
if (s === "connected") fireState("listening");
|
|
990
|
+
if (s === "failed" || s === "disconnected") {
|
|
991
|
+
opts.onError?.({ code: "socket_error", message: `webrtc connection ${s}` });
|
|
992
|
+
teardown();
|
|
998
993
|
}
|
|
999
|
-
if (s ===
|
|
1000
|
-
}
|
|
1001
|
-
await pc.setLocalDescription(await pc.createOffer())
|
|
994
|
+
if (s === "closed" && !ended) teardown();
|
|
995
|
+
};
|
|
996
|
+
await pc.setLocalDescription(await pc.createOffer());
|
|
1002
997
|
try {
|
|
1003
998
|
const offerRes = await fetch(offerUrl, {
|
|
1004
|
-
method:
|
|
1005
|
-
headers: {
|
|
1006
|
-
body: JSON.stringify({ sdp: pc.localDescription.sdp, type:
|
|
1007
|
-
})
|
|
999
|
+
method: "POST",
|
|
1000
|
+
headers: { "content-type": "application/json" },
|
|
1001
|
+
body: JSON.stringify({ sdp: pc.localDescription.sdp, type: "offer", agentId: opts.agentId })
|
|
1002
|
+
});
|
|
1008
1003
|
if (!offerRes.ok) {
|
|
1009
|
-
const code = offerRes.status === 401 ?
|
|
1010
|
-
opts.onError?.({ code, message: `signaling failed: HTTP ${offerRes.status}` })
|
|
1011
|
-
fireState(
|
|
1012
|
-
mic.getTracks().forEach((t) => t.stop())
|
|
1013
|
-
pc.close()
|
|
1014
|
-
audioEl.remove()
|
|
1015
|
-
throw new Error(`webrtc offer failed: ${offerRes.status}`)
|
|
1004
|
+
const code = offerRes.status === 401 ? "unauthorized" : "server_error";
|
|
1005
|
+
opts.onError?.({ code, message: `signaling failed: HTTP ${offerRes.status}` });
|
|
1006
|
+
fireState("error");
|
|
1007
|
+
mic.getTracks().forEach((t) => t.stop());
|
|
1008
|
+
pc.close();
|
|
1009
|
+
audioEl.remove();
|
|
1010
|
+
throw new Error(`webrtc offer failed: ${offerRes.status}`);
|
|
1016
1011
|
}
|
|
1017
|
-
const body = await offerRes.json()
|
|
1018
|
-
callId = body.callId
|
|
1019
|
-
await pc.setRemoteDescription({ type:
|
|
1020
|
-
while (pendingCandidates.length > 0) postCandidate(pendingCandidates.shift())
|
|
1012
|
+
const body = await offerRes.json();
|
|
1013
|
+
callId = body.callId;
|
|
1014
|
+
await pc.setRemoteDescription({ type: "answer", sdp: body.sdp });
|
|
1015
|
+
while (pendingCandidates.length > 0) postCandidate(pendingCandidates.shift());
|
|
1021
1016
|
} catch (err) {
|
|
1022
1017
|
if (!ended) {
|
|
1023
1018
|
opts.onError?.({
|
|
1024
|
-
code:
|
|
1025
|
-
message: err instanceof Error ? err.message :
|
|
1026
|
-
})
|
|
1027
|
-
fireState(
|
|
1028
|
-
mic.getTracks().forEach((t) => t.stop())
|
|
1029
|
-
pc.close()
|
|
1030
|
-
audioEl.remove()
|
|
1019
|
+
code: "network_unreachable",
|
|
1020
|
+
message: err instanceof Error ? err.message : "signaling failed"
|
|
1021
|
+
});
|
|
1022
|
+
fireState("error");
|
|
1023
|
+
mic.getTracks().forEach((t) => t.stop());
|
|
1024
|
+
pc.close();
|
|
1025
|
+
audioEl.remove();
|
|
1031
1026
|
}
|
|
1032
|
-
throw err
|
|
1027
|
+
throw err;
|
|
1033
1028
|
}
|
|
1034
1029
|
return {
|
|
1035
1030
|
get state() {
|
|
1036
|
-
return proto.state
|
|
1031
|
+
return proto.state;
|
|
1037
1032
|
},
|
|
1038
1033
|
get transcript() {
|
|
1039
|
-
return proto.transcript.slice()
|
|
1034
|
+
return proto.transcript.slice();
|
|
1040
1035
|
},
|
|
1041
1036
|
get isMuted() {
|
|
1042
|
-
return muted
|
|
1037
|
+
return muted;
|
|
1043
1038
|
},
|
|
1044
1039
|
end: () => teardown(),
|
|
1045
1040
|
mute: () => {
|
|
1046
|
-
if (muted) return
|
|
1047
|
-
muted = true
|
|
1048
|
-
mic.getAudioTracks().forEach((t) =>
|
|
1041
|
+
if (muted) return;
|
|
1042
|
+
muted = true;
|
|
1043
|
+
mic.getAudioTracks().forEach((t) => t.enabled = false);
|
|
1049
1044
|
},
|
|
1050
1045
|
unmute: () => {
|
|
1051
|
-
if (!muted) return
|
|
1052
|
-
muted = false
|
|
1053
|
-
mic.getAudioTracks().forEach((t) =>
|
|
1054
|
-
}
|
|
1055
|
-
}
|
|
1046
|
+
if (!muted) return;
|
|
1047
|
+
muted = false;
|
|
1048
|
+
mic.getAudioTracks().forEach((t) => t.enabled = true);
|
|
1049
|
+
}
|
|
1050
|
+
};
|
|
1056
1051
|
}
|
|
1057
1052
|
|
|
1058
1053
|
// src/room.ts
|
|
1059
|
-
var import_livekit_client = require(
|
|
1054
|
+
var import_livekit_client = require("livekit-client");
|
|
1060
1055
|
|
|
1061
1056
|
// src/roomProtocol.ts
|
|
1062
|
-
var SYSTEM_TOPIC =
|
|
1063
|
-
var TRANSCRIPT_TOPIC =
|
|
1057
|
+
var SYSTEM_TOPIC = "system";
|
|
1058
|
+
var TRANSCRIPT_TOPIC = "transcript";
|
|
1064
1059
|
var decodeSystem = (bytes) => {
|
|
1065
1060
|
try {
|
|
1066
|
-
const v = JSON.parse(new TextDecoder().decode(bytes))
|
|
1067
|
-
if (v && typeof v.kind ===
|
|
1068
|
-
return null
|
|
1061
|
+
const v = JSON.parse(new TextDecoder().decode(bytes));
|
|
1062
|
+
if (v && typeof v.kind === "string") return v;
|
|
1063
|
+
return null;
|
|
1069
1064
|
} catch {
|
|
1070
|
-
return null
|
|
1065
|
+
return null;
|
|
1071
1066
|
}
|
|
1072
|
-
}
|
|
1067
|
+
};
|
|
1073
1068
|
var decodeTranscript = (bytes) => {
|
|
1074
1069
|
try {
|
|
1075
|
-
const v = JSON.parse(new TextDecoder().decode(bytes))
|
|
1076
|
-
if (v && v.kind ===
|
|
1077
|
-
return null
|
|
1070
|
+
const v = JSON.parse(new TextDecoder().decode(bytes));
|
|
1071
|
+
if (v && v.kind === "partial") return v;
|
|
1072
|
+
return null;
|
|
1078
1073
|
} catch {
|
|
1079
|
-
return null
|
|
1074
|
+
return null;
|
|
1080
1075
|
}
|
|
1081
|
-
}
|
|
1076
|
+
};
|
|
1082
1077
|
|
|
1083
1078
|
// src/room.ts
|
|
1084
|
-
var identityToPid = (identity) =>
|
|
1085
|
-
identity.startsWith('guest:') ? identity.slice('guest:'.length) : identity
|
|
1079
|
+
var identityToPid = (identity) => identity.startsWith("guest:") ? identity.slice("guest:".length) : identity;
|
|
1086
1080
|
var joinRoom = async (opts) => {
|
|
1087
|
-
const exchangeUrl = `${opts.apiBase.replace(/\/+$/,
|
|
1088
|
-
opts.roomId
|
|
1089
|
-
)}/join
|
|
1081
|
+
const exchangeUrl = `${opts.apiBase.replace(/\/+$/, "")}/v1/rooms/${encodeURIComponent(
|
|
1082
|
+
opts.roomId
|
|
1083
|
+
)}/join`;
|
|
1090
1084
|
const exchangeRes = await fetch(exchangeUrl, {
|
|
1091
|
-
method:
|
|
1092
|
-
headers: {
|
|
1093
|
-
body: JSON.stringify({ code: opts.joinCode, name: opts.name })
|
|
1094
|
-
})
|
|
1085
|
+
method: "POST",
|
|
1086
|
+
headers: { "Content-Type": "application/json" },
|
|
1087
|
+
body: JSON.stringify({ code: opts.joinCode, name: opts.name })
|
|
1088
|
+
});
|
|
1095
1089
|
if (!exchangeRes.ok) {
|
|
1096
|
-
const err = await exchangeRes.json().catch(() => ({}))
|
|
1097
|
-
throw new Error(err.error?.code ?? `join_failed_${exchangeRes.status}`)
|
|
1090
|
+
const err = await exchangeRes.json().catch(() => ({}));
|
|
1091
|
+
throw new Error(err.error?.code ?? `join_failed_${exchangeRes.status}`);
|
|
1098
1092
|
}
|
|
1099
|
-
const exchange = await exchangeRes.json()
|
|
1100
|
-
const handlers = /* @__PURE__ */ new Map()
|
|
1093
|
+
const exchange = await exchangeRes.json();
|
|
1094
|
+
const handlers = /* @__PURE__ */ new Map();
|
|
1101
1095
|
const emit = (e, payload) => {
|
|
1102
1096
|
handlers.get(e)?.forEach((h) => {
|
|
1103
1097
|
try {
|
|
1104
|
-
h(payload)
|
|
1105
|
-
} catch {
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
room.
|
|
1110
|
-
|
|
1098
|
+
h(payload);
|
|
1099
|
+
} catch {
|
|
1100
|
+
}
|
|
1101
|
+
});
|
|
1102
|
+
};
|
|
1103
|
+
const room = new import_livekit_client.Room({ adaptiveStream: true, dynacast: true });
|
|
1104
|
+
room.on(
|
|
1105
|
+
import_livekit_client.RoomEvent.ParticipantConnected,
|
|
1106
|
+
(p) => emit("participant.joined", {
|
|
1111
1107
|
participantId: identityToPid(p.identity),
|
|
1112
|
-
name: p.name ??
|
|
1113
|
-
})
|
|
1114
|
-
)
|
|
1115
|
-
room.on(
|
|
1116
|
-
|
|
1108
|
+
name: p.name ?? ""
|
|
1109
|
+
})
|
|
1110
|
+
);
|
|
1111
|
+
room.on(
|
|
1112
|
+
import_livekit_client.RoomEvent.ParticipantDisconnected,
|
|
1113
|
+
(p) => emit("participant.left", {
|
|
1117
1114
|
participantId: identityToPid(p.identity),
|
|
1118
|
-
name: p.name ??
|
|
1119
|
-
})
|
|
1120
|
-
)
|
|
1121
|
-
room.on(import_livekit_client.RoomEvent.Disconnected, () => emit(
|
|
1115
|
+
name: p.name ?? ""
|
|
1116
|
+
})
|
|
1117
|
+
);
|
|
1118
|
+
room.on(import_livekit_client.RoomEvent.Disconnected, () => emit("room.ended", void 0));
|
|
1122
1119
|
room.on(import_livekit_client.RoomEvent.DataReceived, (data, _participant, _kind, topic) => {
|
|
1123
1120
|
if (topic === SYSTEM_TOPIC) {
|
|
1124
|
-
const m = decodeSystem(data)
|
|
1125
|
-
if (m) emit(
|
|
1121
|
+
const m = decodeSystem(data);
|
|
1122
|
+
if (m) emit("system.message", m);
|
|
1126
1123
|
} else if (topic === TRANSCRIPT_TOPIC) {
|
|
1127
|
-
const m = decodeTranscript(data)
|
|
1128
|
-
if (m) emit(
|
|
1124
|
+
const m = decodeTranscript(data);
|
|
1125
|
+
if (m) emit("transcript.partial", m);
|
|
1129
1126
|
}
|
|
1130
|
-
})
|
|
1131
|
-
const trackKind = (t) =>
|
|
1127
|
+
});
|
|
1128
|
+
const trackKind = (t) => t.kind === import_livekit_client.Track.Kind.Video ? "video" : "audio";
|
|
1132
1129
|
const trackSource = (s) => {
|
|
1133
1130
|
switch (s) {
|
|
1134
1131
|
case import_livekit_client.Track.Source.Camera:
|
|
1135
|
-
return
|
|
1132
|
+
return "camera";
|
|
1136
1133
|
case import_livekit_client.Track.Source.Microphone:
|
|
1137
|
-
return
|
|
1134
|
+
return "microphone";
|
|
1138
1135
|
case import_livekit_client.Track.Source.ScreenShare:
|
|
1139
|
-
return
|
|
1136
|
+
return "screen_share";
|
|
1140
1137
|
case import_livekit_client.Track.Source.ScreenShareAudio:
|
|
1141
|
-
return
|
|
1138
|
+
return "screen_share_audio";
|
|
1142
1139
|
default:
|
|
1143
|
-
return
|
|
1140
|
+
return "unknown";
|
|
1144
1141
|
}
|
|
1145
|
-
}
|
|
1146
|
-
room.on(
|
|
1147
|
-
|
|
1142
|
+
};
|
|
1143
|
+
room.on(
|
|
1144
|
+
import_livekit_client.RoomEvent.TrackSubscribed,
|
|
1145
|
+
(track, pub, participant) => emit("track.subscribed", {
|
|
1148
1146
|
participantId: identityToPid(participant.identity),
|
|
1149
1147
|
kind: trackKind(track),
|
|
1150
1148
|
source: trackSource(pub.source),
|
|
1151
|
-
track
|
|
1152
|
-
})
|
|
1153
|
-
)
|
|
1154
|
-
room.on(
|
|
1155
|
-
|
|
1149
|
+
track
|
|
1150
|
+
})
|
|
1151
|
+
);
|
|
1152
|
+
room.on(
|
|
1153
|
+
import_livekit_client.RoomEvent.TrackUnsubscribed,
|
|
1154
|
+
(track, pub, participant) => emit("track.unsubscribed", {
|
|
1156
1155
|
participantId: identityToPid(participant.identity),
|
|
1157
1156
|
kind: trackKind(track),
|
|
1158
1157
|
source: trackSource(pub.source),
|
|
1159
|
-
track
|
|
1160
|
-
})
|
|
1161
|
-
)
|
|
1162
|
-
room.on(
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
speakers
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1158
|
+
track
|
|
1159
|
+
})
|
|
1160
|
+
);
|
|
1161
|
+
room.on(
|
|
1162
|
+
import_livekit_client.RoomEvent.ActiveSpeakersChanged,
|
|
1163
|
+
(speakers) => emit(
|
|
1164
|
+
"active.speakers",
|
|
1165
|
+
speakers.map((p) => identityToPid(p.identity))
|
|
1166
|
+
)
|
|
1167
|
+
);
|
|
1168
|
+
await room.connect(exchange.livekit.url, exchange.livekit.token);
|
|
1169
1169
|
return {
|
|
1170
1170
|
participantId: exchange.participantId,
|
|
1171
1171
|
get participants() {
|
|
1172
1172
|
return [...room.remoteParticipants.values()].map((p) => ({
|
|
1173
1173
|
participantId: identityToPid(p.identity),
|
|
1174
|
-
name: p.name ??
|
|
1175
|
-
}))
|
|
1174
|
+
name: p.name ?? ""
|
|
1175
|
+
}));
|
|
1176
1176
|
},
|
|
1177
1177
|
on(event, handler) {
|
|
1178
|
-
const set = handlers.get(event) ?? /* @__PURE__ */ new Set()
|
|
1179
|
-
set.add(handler)
|
|
1180
|
-
handlers.set(event, set)
|
|
1178
|
+
const set = handlers.get(event) ?? /* @__PURE__ */ new Set();
|
|
1179
|
+
set.add(handler);
|
|
1180
|
+
handlers.set(event, set);
|
|
1181
1181
|
},
|
|
1182
1182
|
publishMic: async () => {
|
|
1183
|
-
await room.localParticipant.setMicrophoneEnabled(true)
|
|
1183
|
+
await room.localParticipant.setMicrophoneEnabled(true);
|
|
1184
1184
|
},
|
|
1185
1185
|
publishCamera: async () => {
|
|
1186
|
-
await room.localParticipant.setCameraEnabled(true)
|
|
1186
|
+
await room.localParticipant.setCameraEnabled(true);
|
|
1187
1187
|
},
|
|
1188
1188
|
setMicEnabled: async (on) => {
|
|
1189
|
-
await room.localParticipant.setMicrophoneEnabled(on)
|
|
1189
|
+
await room.localParticipant.setMicrophoneEnabled(on);
|
|
1190
1190
|
},
|
|
1191
1191
|
setCameraEnabled: async (on) => {
|
|
1192
|
-
await room.localParticipant.setCameraEnabled(on)
|
|
1192
|
+
await room.localParticipant.setCameraEnabled(on);
|
|
1193
1193
|
},
|
|
1194
1194
|
isMicEnabled: () => room.localParticipant.isMicrophoneEnabled,
|
|
1195
1195
|
isCameraEnabled: () => room.localParticipant.isCameraEnabled,
|
|
1196
|
-
getLocalCameraTrack: () =>
|
|
1197
|
-
room.localParticipant.getTrackPublication(import_livekit_client.Track.Source.Camera)
|
|
1198
|
-
?.videoTrack ?? null,
|
|
1196
|
+
getLocalCameraTrack: () => room.localParticipant.getTrackPublication(import_livekit_client.Track.Source.Camera)?.videoTrack ?? null,
|
|
1199
1197
|
getRemoteTracks: () => {
|
|
1200
|
-
const out = []
|
|
1198
|
+
const out = [];
|
|
1201
1199
|
for (const p of room.remoteParticipants.values()) {
|
|
1202
1200
|
for (const pub of p.trackPublications.values()) {
|
|
1203
|
-
const track = pub.track
|
|
1204
|
-
if (!track) continue
|
|
1201
|
+
const track = pub.track;
|
|
1202
|
+
if (!track) continue;
|
|
1205
1203
|
out.push({
|
|
1206
1204
|
participantId: identityToPid(p.identity),
|
|
1207
1205
|
kind: trackKind(track),
|
|
1208
1206
|
source: trackSource(pub.source),
|
|
1209
|
-
track
|
|
1210
|
-
})
|
|
1207
|
+
track
|
|
1208
|
+
});
|
|
1211
1209
|
}
|
|
1212
1210
|
}
|
|
1213
|
-
return out
|
|
1211
|
+
return out;
|
|
1214
1212
|
},
|
|
1215
1213
|
setScreenShareEnabled: async (on, opts2) => {
|
|
1216
|
-
await room.localParticipant.setScreenShareEnabled(on, { audio: opts2?.audio ?? false })
|
|
1214
|
+
await room.localParticipant.setScreenShareEnabled(on, { audio: opts2?.audio ?? false });
|
|
1217
1215
|
},
|
|
1218
1216
|
isScreenShareEnabled: () => room.localParticipant.isScreenShareEnabled,
|
|
1219
|
-
getLocalScreenTrack: () =>
|
|
1220
|
-
room.localParticipant.getTrackPublication(import_livekit_client.Track.Source.ScreenShare)
|
|
1221
|
-
?.videoTrack ?? null,
|
|
1217
|
+
getLocalScreenTrack: () => room.localParticipant.getTrackPublication(import_livekit_client.Track.Source.ScreenShare)?.videoTrack ?? null,
|
|
1222
1218
|
leave: async () => {
|
|
1223
|
-
await room.disconnect()
|
|
1219
|
+
await room.disconnect();
|
|
1220
|
+
}
|
|
1221
|
+
};
|
|
1222
|
+
};
|
|
1223
|
+
|
|
1224
|
+
// src/textSession.ts
|
|
1225
|
+
async function startTextSession(opts) {
|
|
1226
|
+
const f = opts.fetch ?? fetch;
|
|
1227
|
+
const tokenQs = `?token=${encodeURIComponent(opts.token)}`;
|
|
1228
|
+
const startUrl = `${opts.baseUrl}/v1/agents/${opts.agentId}/chat${tokenQs}`;
|
|
1229
|
+
const startBody = opts.text ? JSON.stringify({ text: opts.text }) : "{}";
|
|
1230
|
+
const res = await f(startUrl, {
|
|
1231
|
+
method: "POST",
|
|
1232
|
+
headers: { "Content-Type": "application/json", Accept: "text/event-stream" },
|
|
1233
|
+
body: startBody
|
|
1234
|
+
});
|
|
1235
|
+
if (!res.ok || !res.body) {
|
|
1236
|
+
const text = await res.text().catch(() => "");
|
|
1237
|
+
throw new Error(`startTextSession failed: ${res.status} ${text}`);
|
|
1238
|
+
}
|
|
1239
|
+
const iter = parseSse(res.body);
|
|
1240
|
+
let chatId = "";
|
|
1241
|
+
let callId = "";
|
|
1242
|
+
const buffered = [];
|
|
1243
|
+
const it = iter[Symbol.asyncIterator]();
|
|
1244
|
+
while (true) {
|
|
1245
|
+
const { value, done } = await it.next();
|
|
1246
|
+
if (done) break;
|
|
1247
|
+
if (value.type === "chat.started") {
|
|
1248
|
+
chatId = value.chatId;
|
|
1249
|
+
callId = value.callId;
|
|
1250
|
+
break;
|
|
1251
|
+
}
|
|
1252
|
+
buffered.push(value);
|
|
1253
|
+
}
|
|
1254
|
+
return {
|
|
1255
|
+
id: chatId,
|
|
1256
|
+
callId,
|
|
1257
|
+
greeting: replayThen(buffered, { [Symbol.asyncIterator]: () => it }),
|
|
1258
|
+
async send(text) {
|
|
1259
|
+
const r = await f(`${opts.baseUrl}/v1/chats/${chatId}/messages${tokenQs}`, {
|
|
1260
|
+
method: "POST",
|
|
1261
|
+
headers: { "Content-Type": "application/json", Accept: "text/event-stream" },
|
|
1262
|
+
body: JSON.stringify({ text })
|
|
1263
|
+
});
|
|
1264
|
+
if (!r.ok || !r.body) {
|
|
1265
|
+
const errText = await r.text().catch(() => "");
|
|
1266
|
+
throw new Error(`send failed: ${r.status} ${errText}`);
|
|
1267
|
+
}
|
|
1268
|
+
return parseSse(r.body);
|
|
1224
1269
|
},
|
|
1270
|
+
async end() {
|
|
1271
|
+
await f(`${opts.baseUrl}/v1/calls/${callId}`, { method: "DELETE" });
|
|
1272
|
+
}
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
async function* parseSse(body) {
|
|
1276
|
+
const reader = body.getReader();
|
|
1277
|
+
const decoder = new TextDecoder();
|
|
1278
|
+
let buf = "";
|
|
1279
|
+
while (true) {
|
|
1280
|
+
const { value, done } = await reader.read();
|
|
1281
|
+
if (done) return;
|
|
1282
|
+
buf += decoder.decode(value, { stream: true });
|
|
1283
|
+
let idx;
|
|
1284
|
+
while ((idx = buf.indexOf("\n\n")) >= 0) {
|
|
1285
|
+
const chunk = buf.slice(0, idx);
|
|
1286
|
+
buf = buf.slice(idx + 2);
|
|
1287
|
+
let event = "message";
|
|
1288
|
+
let data = "";
|
|
1289
|
+
for (const line of chunk.split("\n")) {
|
|
1290
|
+
if (line.startsWith(":")) continue;
|
|
1291
|
+
if (line.startsWith("event:")) event = line.slice(6).trim();
|
|
1292
|
+
else if (line.startsWith("data:")) data += line.slice(5).trim();
|
|
1293
|
+
}
|
|
1294
|
+
if (!data) continue;
|
|
1295
|
+
try {
|
|
1296
|
+
const parsed = JSON.parse(data);
|
|
1297
|
+
yield { type: event, ...parsed };
|
|
1298
|
+
} catch {
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1225
1301
|
}
|
|
1226
1302
|
}
|
|
1303
|
+
async function* replayThen(buffered, rest) {
|
|
1304
|
+
for (const x of buffered) yield x;
|
|
1305
|
+
for await (const x of rest) yield x;
|
|
1306
|
+
}
|
|
1227
1307
|
|
|
1228
1308
|
// src/incomingCall.ts
|
|
1229
1309
|
var parseIncomingCall = (raw) => {
|
|
1230
|
-
if (typeof raw !==
|
|
1231
|
-
throw new Error(
|
|
1232
|
-
}
|
|
1233
|
-
const p = raw
|
|
1234
|
-
if (typeof p.token !==
|
|
1235
|
-
throw new Error(
|
|
1236
|
-
}
|
|
1237
|
-
if (typeof p.agentId !==
|
|
1238
|
-
throw new Error(
|
|
1239
|
-
}
|
|
1240
|
-
const transport = p.transport ===
|
|
1241
|
-
const out = { token: p.token, agentId: p.agentId, transport }
|
|
1242
|
-
if (transport ===
|
|
1243
|
-
out.webrtcGatewayBase = p.webrtcGatewayBase
|
|
1244
|
-
}
|
|
1245
|
-
if (typeof p.expiresAt ===
|
|
1246
|
-
if (typeof p.agentName ===
|
|
1247
|
-
if (typeof p.agentAvatarUrl ===
|
|
1248
|
-
return out
|
|
1249
|
-
}
|
|
1310
|
+
if (typeof raw !== "object" || raw === null) {
|
|
1311
|
+
throw new Error("parseIncomingCall: payload must be an object");
|
|
1312
|
+
}
|
|
1313
|
+
const p = raw;
|
|
1314
|
+
if (typeof p.token !== "string" || !p.token.startsWith("ct_")) {
|
|
1315
|
+
throw new Error("parseIncomingCall: missing or invalid `token` (expected a ct_ string)");
|
|
1316
|
+
}
|
|
1317
|
+
if (typeof p.agentId !== "string" || p.agentId.length === 0) {
|
|
1318
|
+
throw new Error("parseIncomingCall: missing `agentId`");
|
|
1319
|
+
}
|
|
1320
|
+
const transport = p.transport === "webrtc" ? "webrtc" : "ws";
|
|
1321
|
+
const out = { token: p.token, agentId: p.agentId, transport };
|
|
1322
|
+
if (transport === "webrtc" && typeof p.webrtcGatewayBase === "string") {
|
|
1323
|
+
out.webrtcGatewayBase = p.webrtcGatewayBase;
|
|
1324
|
+
}
|
|
1325
|
+
if (typeof p.expiresAt === "number") out.expiresAt = p.expiresAt;
|
|
1326
|
+
if (typeof p.agentName === "string") out.agentName = p.agentName;
|
|
1327
|
+
if (typeof p.agentAvatarUrl === "string") out.agentAvatarUrl = p.agentAvatarUrl;
|
|
1328
|
+
return out;
|
|
1329
|
+
};
|
|
1250
1330
|
|
|
1251
1331
|
// src/browser.ts
|
|
1252
|
-
var browserWsFactory = (url) => new globalThis.WebSocket(url)
|
|
1332
|
+
var browserWsFactory = (url) => new globalThis.WebSocket(url);
|
|
1253
1333
|
var BrowserVoiceFactory = class {
|
|
1254
1334
|
constructor(config) {
|
|
1255
1335
|
this.startCall = async (options) => {
|
|
1256
1336
|
if (!options.agentId) {
|
|
1257
|
-
throw new Error(
|
|
1337
|
+
throw new Error("startCall: agentId is required");
|
|
1258
1338
|
}
|
|
1259
|
-
const { context, metadata } = mergeStartCallContext(this.config, options)
|
|
1339
|
+
const { context, metadata } = mergeStartCallContext(this.config, options);
|
|
1260
1340
|
const fetchArgs = {
|
|
1261
1341
|
agentId: options.agentId,
|
|
1262
1342
|
userId: options.userId,
|
|
1263
1343
|
context,
|
|
1264
|
-
metadata
|
|
1265
|
-
}
|
|
1266
|
-
let resolved
|
|
1344
|
+
metadata
|
|
1345
|
+
};
|
|
1346
|
+
let resolved;
|
|
1267
1347
|
if (options.token) {
|
|
1268
|
-
resolved = { token: options.token, transport:
|
|
1348
|
+
resolved = { token: options.token, transport: "ws" };
|
|
1269
1349
|
} else {
|
|
1270
|
-
const r = await this.config.fetchToken(fetchArgs)
|
|
1350
|
+
const r = await this.config.fetchToken(fetchArgs);
|
|
1271
1351
|
if (!r) {
|
|
1272
|
-
throw new Error(
|
|
1352
|
+
throw new Error("configureVoiceClient.fetchToken returned empty token");
|
|
1273
1353
|
}
|
|
1274
|
-
resolved = typeof r ===
|
|
1354
|
+
resolved = typeof r === "string" ? { token: r, transport: "ws" } : r;
|
|
1275
1355
|
if (!resolved.token) {
|
|
1276
|
-
throw new Error(
|
|
1356
|
+
throw new Error("configureVoiceClient.fetchToken returned an object without `token`");
|
|
1277
1357
|
}
|
|
1278
1358
|
}
|
|
1279
|
-
if (resolved.transport ===
|
|
1359
|
+
if (resolved.transport === "webrtc") {
|
|
1280
1360
|
return createWebRtcCall({
|
|
1281
1361
|
agentId: options.agentId,
|
|
1282
1362
|
apiBase: this.config.apiBase,
|
|
@@ -1288,13 +1368,11 @@ var BrowserVoiceFactory = class {
|
|
|
1288
1368
|
// Synthesise a minimal CallEndEvent. WebRTC doesn't carry an end reason
|
|
1289
1369
|
// from the server yet — use 'agent_ended' as placeholder. durationMs is
|
|
1290
1370
|
// tracked at 0 until the followup lands (see spec Followups section).
|
|
1291
|
-
onEnd: options.onEnd
|
|
1292
|
-
? () => options.onEnd({ reason: 'agent_ended', durationMs: 0 })
|
|
1293
|
-
: void 0,
|
|
1371
|
+
onEnd: options.onEnd ? () => options.onEnd({ reason: "agent_ended", durationMs: 0 }) : void 0,
|
|
1294
1372
|
onInterrupt: options.onInterrupt,
|
|
1295
1373
|
onAgentTurnStart: options.onAgentTurnStart,
|
|
1296
|
-
clientTools: options.clientTools
|
|
1297
|
-
})
|
|
1374
|
+
clientTools: options.clientTools
|
|
1375
|
+
});
|
|
1298
1376
|
}
|
|
1299
1377
|
const client = new BrowserVoiceClient({
|
|
1300
1378
|
config: this.config,
|
|
@@ -1302,11 +1380,11 @@ var BrowserVoiceFactory = class {
|
|
|
1302
1380
|
// see what the SDK saw.
|
|
1303
1381
|
options: { ...options, context, metadata },
|
|
1304
1382
|
token: resolved.token,
|
|
1305
|
-
wsFactory: browserWsFactory
|
|
1306
|
-
})
|
|
1307
|
-
await client.start()
|
|
1308
|
-
return client
|
|
1309
|
-
}
|
|
1383
|
+
wsFactory: browserWsFactory
|
|
1384
|
+
});
|
|
1385
|
+
await client.start();
|
|
1386
|
+
return client;
|
|
1387
|
+
};
|
|
1310
1388
|
// Multi-party rooms (Phase 7 video).
|
|
1311
1389
|
//
|
|
1312
1390
|
// The guest's browser calls this with the roomId + joinCode it parsed
|
|
@@ -1314,24 +1392,31 @@ var BrowserVoiceFactory = class {
|
|
|
1314
1392
|
// JWT against `${apiBase}/v1/rooms/:roomId/join` (an AUTH-EXEMPT
|
|
1315
1393
|
// endpoint — the opaque code is the only credential), then connects
|
|
1316
1394
|
// to LiveKit and returns a typed event surface.
|
|
1317
|
-
this.joinRoom = (opts) => joinRoom({ apiBase: this.config.apiBase, ...opts })
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1395
|
+
this.joinRoom = (opts) => joinRoom({ apiBase: this.config.apiBase, ...opts });
|
|
1396
|
+
// Text-channel chat session (no microphone / audio).
|
|
1397
|
+
// Mint a `ct_` token with `channel: 'text'` on your backend, then call
|
|
1398
|
+
// this to open an SSE stream against the chat API.
|
|
1399
|
+
this.startTextSession = (opts) => startTextSession({
|
|
1400
|
+
...opts,
|
|
1401
|
+
baseUrl: this.config.apiBase
|
|
1402
|
+
});
|
|
1403
|
+
this.config = config;
|
|
1404
|
+
}
|
|
1405
|
+
};
|
|
1321
1406
|
function configureVoiceClient(config) {
|
|
1322
|
-
return new BrowserVoiceFactory(normalizeConfig(config))
|
|
1407
|
+
return new BrowserVoiceFactory(normalizeConfig(config));
|
|
1323
1408
|
}
|
|
1324
1409
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1325
|
-
0 &&
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
//# sourceMappingURL=browser.js.map
|
|
1410
|
+
0 && (module.exports = {
|
|
1411
|
+
buildWsUrl,
|
|
1412
|
+
configureVoiceClient,
|
|
1413
|
+
createAudioCapture,
|
|
1414
|
+
createAudioPlayback,
|
|
1415
|
+
createProtocolState,
|
|
1416
|
+
createReconnectingWebSocket,
|
|
1417
|
+
handleServerMessage,
|
|
1418
|
+
joinRoom,
|
|
1419
|
+
parseIncomingCall,
|
|
1420
|
+
startTextSession
|
|
1421
|
+
});
|
|
1422
|
+
//# sourceMappingURL=browser.js.map
|