@pipecat-ai/daily-transport 0.3.4 → 0.3.6

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.
@@ -7,35 +7,1541 @@ function $parcel$export(e, n, v, s) {
7
7
  }
8
8
  var $683f111f61e07358$exports = {};
9
9
 
10
+ $parcel$export($683f111f61e07358$exports, "DailyRTVIMessageType", () => $683f111f61e07358$export$ef180de88fd317cc);
10
11
  $parcel$export($683f111f61e07358$exports, "DailyTransport", () => $683f111f61e07358$export$b1ca982aa1e488c1);
11
12
 
12
13
 
14
+ /**
15
+ * Raw wav audio file contents
16
+ * @typedef {Object} WavPackerAudioType
17
+ * @property {Blob} blob
18
+ * @property {string} url
19
+ * @property {number} channelCount
20
+ * @property {number} sampleRate
21
+ * @property {number} duration
22
+ */ /**
23
+ * Utility class for assembling PCM16 "audio/wav" data
24
+ * @class
25
+ */ class $6d4b7449a1e1544a$export$13afda237b1c9846 {
26
+ /**
27
+ * Converts Float32Array of amplitude data to ArrayBuffer in Int16Array format
28
+ * @param {Float32Array} float32Array
29
+ * @returns {ArrayBuffer}
30
+ */ static floatTo16BitPCM(float32Array) {
31
+ const buffer = new ArrayBuffer(float32Array.length * 2);
32
+ const view = new DataView(buffer);
33
+ let offset = 0;
34
+ for(let i = 0; i < float32Array.length; i++, offset += 2){
35
+ let s = Math.max(-1, Math.min(1, float32Array[i]));
36
+ view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true);
37
+ }
38
+ return buffer;
39
+ }
40
+ /**
41
+ * Concatenates two ArrayBuffers
42
+ * @param {ArrayBuffer} leftBuffer
43
+ * @param {ArrayBuffer} rightBuffer
44
+ * @returns {ArrayBuffer}
45
+ */ static mergeBuffers(leftBuffer, rightBuffer) {
46
+ const tmpArray = new Uint8Array(leftBuffer.byteLength + rightBuffer.byteLength);
47
+ tmpArray.set(new Uint8Array(leftBuffer), 0);
48
+ tmpArray.set(new Uint8Array(rightBuffer), leftBuffer.byteLength);
49
+ return tmpArray.buffer;
50
+ }
51
+ /**
52
+ * Packs data into an Int16 format
53
+ * @private
54
+ * @param {number} size 0 = 1x Int16, 1 = 2x Int16
55
+ * @param {number} arg value to pack
56
+ * @returns
57
+ */ _packData(size, arg) {
58
+ return [
59
+ new Uint8Array([
60
+ arg,
61
+ arg >> 8
62
+ ]),
63
+ new Uint8Array([
64
+ arg,
65
+ arg >> 8,
66
+ arg >> 16,
67
+ arg >> 24
68
+ ])
69
+ ][size];
70
+ }
71
+ /**
72
+ * Packs audio into "audio/wav" Blob
73
+ * @param {number} sampleRate
74
+ * @param {{bitsPerSample: number, channels: Array<Float32Array>, data: Int16Array}} audio
75
+ * @returns {WavPackerAudioType}
76
+ */ pack(sampleRate, audio) {
77
+ if (!audio?.bitsPerSample) throw new Error(`Missing "bitsPerSample"`);
78
+ else if (!audio?.channels) throw new Error(`Missing "channels"`);
79
+ else if (!audio?.data) throw new Error(`Missing "data"`);
80
+ const { bitsPerSample: bitsPerSample, channels: channels, data: data } = audio;
81
+ const output = [
82
+ // Header
83
+ 'RIFF',
84
+ this._packData(1, 52),
85
+ 'WAVE',
86
+ // chunk 1
87
+ 'fmt ',
88
+ this._packData(1, 16),
89
+ this._packData(0, 1),
90
+ this._packData(0, channels.length),
91
+ this._packData(1, sampleRate),
92
+ this._packData(1, sampleRate * channels.length * bitsPerSample / 8),
93
+ this._packData(0, channels.length * bitsPerSample / 8),
94
+ this._packData(0, bitsPerSample),
95
+ // chunk 2
96
+ 'data',
97
+ this._packData(1, channels[0].length * channels.length * bitsPerSample / 8),
98
+ data
99
+ ];
100
+ const blob = new Blob(output, {
101
+ type: 'audio/mpeg'
102
+ });
103
+ const url = URL.createObjectURL(blob);
104
+ return {
105
+ blob: blob,
106
+ url: url,
107
+ channelCount: channels.length,
108
+ sampleRate: sampleRate,
109
+ duration: data.byteLength / (channels.length * sampleRate * 2)
110
+ };
111
+ }
112
+ }
113
+ globalThis.WavPacker = $6d4b7449a1e1544a$export$13afda237b1c9846;
114
+
115
+
116
+ /**
117
+ * Constants for help with visualization
118
+ * Helps map frequency ranges from Fast Fourier Transform
119
+ * to human-interpretable ranges, notably music ranges and
120
+ * human vocal ranges.
121
+ */ // Eighth octave frequencies
122
+ const $03f71ce85e00ada6$var$octave8Frequencies = [
123
+ 4186.01,
124
+ 4434.92,
125
+ 4698.63,
126
+ 4978.03,
127
+ 5274.04,
128
+ 5587.65,
129
+ 5919.91,
130
+ 6271.93,
131
+ 6644.88,
132
+ 7040.0,
133
+ 7458.62,
134
+ 7902.13
135
+ ];
136
+ // Labels for each of the above frequencies
137
+ const $03f71ce85e00ada6$var$octave8FrequencyLabels = [
138
+ 'C',
139
+ 'C#',
140
+ 'D',
141
+ 'D#',
142
+ 'E',
143
+ 'F',
144
+ 'F#',
145
+ 'G',
146
+ 'G#',
147
+ 'A',
148
+ 'A#',
149
+ 'B'
150
+ ];
151
+ const $03f71ce85e00ada6$export$776c63898ae5b636 = [];
152
+ const $03f71ce85e00ada6$export$facd167cc27ea9b0 = [];
153
+ for(let i = 1; i <= 8; i++)for(let f = 0; f < $03f71ce85e00ada6$var$octave8Frequencies.length; f++){
154
+ const freq = $03f71ce85e00ada6$var$octave8Frequencies[f];
155
+ $03f71ce85e00ada6$export$776c63898ae5b636.push(freq / Math.pow(2, 8 - i));
156
+ $03f71ce85e00ada6$export$facd167cc27ea9b0.push($03f71ce85e00ada6$var$octave8FrequencyLabels[f] + i);
157
+ }
158
+ /**
159
+ * Subset of the note frequencies between 32 and 2000 Hz
160
+ * 6 octave range: C1 to B6
161
+ */ const $03f71ce85e00ada6$var$voiceFrequencyRange = [
162
+ 32.0,
163
+ 2000.0
164
+ ];
165
+ const $03f71ce85e00ada6$export$dbc1581ed2cfa183 = $03f71ce85e00ada6$export$776c63898ae5b636.filter((_, i)=>{
166
+ return $03f71ce85e00ada6$export$776c63898ae5b636[i] > $03f71ce85e00ada6$var$voiceFrequencyRange[0] && $03f71ce85e00ada6$export$776c63898ae5b636[i] < $03f71ce85e00ada6$var$voiceFrequencyRange[1];
167
+ });
168
+ const $03f71ce85e00ada6$export$30a6f2881311088f = $03f71ce85e00ada6$export$facd167cc27ea9b0.filter((_, i)=>{
169
+ return $03f71ce85e00ada6$export$776c63898ae5b636[i] > $03f71ce85e00ada6$var$voiceFrequencyRange[0] && $03f71ce85e00ada6$export$776c63898ae5b636[i] < $03f71ce85e00ada6$var$voiceFrequencyRange[1];
170
+ });
171
+
172
+
173
+ class $f32f064564ee62f6$export$2c3136da0bf130f9 {
174
+ /**
175
+ * Retrieves frequency domain data from an AnalyserNode adjusted to a decibel range
176
+ * returns human-readable formatting and labels
177
+ * @param {AnalyserNode} analyser
178
+ * @param {number} sampleRate
179
+ * @param {Float32Array} [fftResult]
180
+ * @param {"frequency"|"music"|"voice"} [analysisType]
181
+ * @param {number} [minDecibels] default -100
182
+ * @param {number} [maxDecibels] default -30
183
+ * @returns {AudioAnalysisOutputType}
184
+ */ static getFrequencies(analyser, sampleRate, fftResult, analysisType = 'frequency', minDecibels = -100, maxDecibels = -30) {
185
+ if (!fftResult) {
186
+ fftResult = new Float32Array(analyser.frequencyBinCount);
187
+ analyser.getFloatFrequencyData(fftResult);
188
+ }
189
+ const nyquistFrequency = sampleRate / 2;
190
+ const frequencyStep = 1 / fftResult.length * nyquistFrequency;
191
+ let outputValues;
192
+ let frequencies;
193
+ let labels;
194
+ if (analysisType === 'music' || analysisType === 'voice') {
195
+ const useFrequencies = analysisType === 'voice' ? (0, $03f71ce85e00ada6$export$dbc1581ed2cfa183) : (0, $03f71ce85e00ada6$export$776c63898ae5b636);
196
+ const aggregateOutput = Array(useFrequencies.length).fill(minDecibels);
197
+ for(let i = 0; i < fftResult.length; i++){
198
+ const frequency = i * frequencyStep;
199
+ const amplitude = fftResult[i];
200
+ for(let n = useFrequencies.length - 1; n >= 0; n--)if (frequency > useFrequencies[n]) {
201
+ aggregateOutput[n] = Math.max(aggregateOutput[n], amplitude);
202
+ break;
203
+ }
204
+ }
205
+ outputValues = aggregateOutput;
206
+ frequencies = analysisType === 'voice' ? (0, $03f71ce85e00ada6$export$dbc1581ed2cfa183) : (0, $03f71ce85e00ada6$export$776c63898ae5b636);
207
+ labels = analysisType === 'voice' ? (0, $03f71ce85e00ada6$export$30a6f2881311088f) : (0, $03f71ce85e00ada6$export$facd167cc27ea9b0);
208
+ } else {
209
+ outputValues = Array.from(fftResult);
210
+ frequencies = outputValues.map((_, i)=>frequencyStep * i);
211
+ labels = frequencies.map((f)=>`${f.toFixed(2)} Hz`);
212
+ }
213
+ // We normalize to {0, 1}
214
+ const normalizedOutput = outputValues.map((v)=>{
215
+ return Math.max(0, Math.min((v - minDecibels) / (maxDecibels - minDecibels), 1));
216
+ });
217
+ const values = new Float32Array(normalizedOutput);
218
+ return {
219
+ values: values,
220
+ frequencies: frequencies,
221
+ labels: labels
222
+ };
223
+ }
224
+ /**
225
+ * Creates a new AudioAnalysis instance for an HTMLAudioElement
226
+ * @param {HTMLAudioElement} audioElement
227
+ * @param {AudioBuffer|null} [audioBuffer] If provided, will cache all frequency domain data from the buffer
228
+ * @returns {AudioAnalysis}
229
+ */ constructor(audioElement, audioBuffer = null){
230
+ this.fftResults = [];
231
+ if (audioBuffer) {
232
+ /**
233
+ * Modified from
234
+ * https://stackoverflow.com/questions/75063715/using-the-web-audio-api-to-analyze-a-song-without-playing
235
+ *
236
+ * We do this to populate FFT values for the audio if provided an `audioBuffer`
237
+ * The reason to do this is that Safari fails when using `createMediaElementSource`
238
+ * This has a non-zero RAM cost so we only opt-in to run it on Safari, Chrome is better
239
+ */ const { length: length, sampleRate: sampleRate } = audioBuffer;
240
+ const offlineAudioContext = new OfflineAudioContext({
241
+ length: length,
242
+ sampleRate: sampleRate
243
+ });
244
+ const source = offlineAudioContext.createBufferSource();
245
+ source.buffer = audioBuffer;
246
+ const analyser = offlineAudioContext.createAnalyser();
247
+ analyser.fftSize = 8192;
248
+ analyser.smoothingTimeConstant = 0.1;
249
+ source.connect(analyser);
250
+ // limit is :: 128 / sampleRate;
251
+ // but we just want 60fps - cuts ~1s from 6MB to 1MB of RAM
252
+ const renderQuantumInSeconds = 1 / 60;
253
+ const durationInSeconds = length / sampleRate;
254
+ const analyze = (index)=>{
255
+ const suspendTime = renderQuantumInSeconds * index;
256
+ if (suspendTime < durationInSeconds) offlineAudioContext.suspend(suspendTime).then(()=>{
257
+ const fftResult = new Float32Array(analyser.frequencyBinCount);
258
+ analyser.getFloatFrequencyData(fftResult);
259
+ this.fftResults.push(fftResult);
260
+ analyze(index + 1);
261
+ });
262
+ if (index === 1) offlineAudioContext.startRendering();
263
+ else offlineAudioContext.resume();
264
+ };
265
+ source.start(0);
266
+ analyze(1);
267
+ this.audio = audioElement;
268
+ this.context = offlineAudioContext;
269
+ this.analyser = analyser;
270
+ this.sampleRate = sampleRate;
271
+ this.audioBuffer = audioBuffer;
272
+ } else {
273
+ const audioContext = new AudioContext();
274
+ const track = audioContext.createMediaElementSource(audioElement);
275
+ const analyser = audioContext.createAnalyser();
276
+ analyser.fftSize = 8192;
277
+ analyser.smoothingTimeConstant = 0.1;
278
+ track.connect(analyser);
279
+ analyser.connect(audioContext.destination);
280
+ this.audio = audioElement;
281
+ this.context = audioContext;
282
+ this.analyser = analyser;
283
+ this.sampleRate = this.context.sampleRate;
284
+ this.audioBuffer = null;
285
+ }
286
+ }
287
+ /**
288
+ * Gets the current frequency domain data from the playing audio track
289
+ * @param {"frequency"|"music"|"voice"} [analysisType]
290
+ * @param {number} [minDecibels] default -100
291
+ * @param {number} [maxDecibels] default -30
292
+ * @returns {AudioAnalysisOutputType}
293
+ */ getFrequencies(analysisType = 'frequency', minDecibels = -100, maxDecibels = -30) {
294
+ let fftResult = null;
295
+ if (this.audioBuffer && this.fftResults.length) {
296
+ const pct = this.audio.currentTime / this.audio.duration;
297
+ const index = Math.min(pct * this.fftResults.length | 0, this.fftResults.length - 1);
298
+ fftResult = this.fftResults[index];
299
+ }
300
+ return $f32f064564ee62f6$export$2c3136da0bf130f9.getFrequencies(this.analyser, this.sampleRate, fftResult, analysisType, minDecibels, maxDecibels);
301
+ }
302
+ /**
303
+ * Resume the internal AudioContext if it was suspended due to the lack of
304
+ * user interaction when the AudioAnalysis was instantiated.
305
+ * @returns {Promise<true>}
306
+ */ async resumeIfSuspended() {
307
+ if (this.context.state === 'suspended') await this.context.resume();
308
+ return true;
309
+ }
310
+ }
311
+ globalThis.AudioAnalysis = $f32f064564ee62f6$export$2c3136da0bf130f9;
312
+
313
+
314
+ const $29a8a70a9466b14f$export$50b76700e2b15e9 = `
315
+ class StreamProcessor extends AudioWorkletProcessor {
316
+ constructor() {
317
+ super();
318
+ this.hasStarted = false;
319
+ this.hasInterrupted = false;
320
+ this.outputBuffers = [];
321
+ this.bufferLength = 128;
322
+ this.write = { buffer: new Float32Array(this.bufferLength), trackId: null };
323
+ this.writeOffset = 0;
324
+ this.trackSampleOffsets = {};
325
+ this.port.onmessage = (event) => {
326
+ if (event.data) {
327
+ const payload = event.data;
328
+ if (payload.event === 'write') {
329
+ const int16Array = payload.buffer;
330
+ const float32Array = new Float32Array(int16Array.length);
331
+ for (let i = 0; i < int16Array.length; i++) {
332
+ float32Array[i] = int16Array[i] / 0x8000; // Convert Int16 to Float32
333
+ }
334
+ this.writeData(float32Array, payload.trackId);
335
+ } else if (
336
+ payload.event === 'offset' ||
337
+ payload.event === 'interrupt'
338
+ ) {
339
+ const requestId = payload.requestId;
340
+ const trackId = this.write.trackId;
341
+ const offset = this.trackSampleOffsets[trackId] || 0;
342
+ this.port.postMessage({
343
+ event: 'offset',
344
+ requestId,
345
+ trackId,
346
+ offset,
347
+ });
348
+ if (payload.event === 'interrupt') {
349
+ this.hasInterrupted = true;
350
+ }
351
+ } else {
352
+ throw new Error(\`Unhandled event "\${payload.event}"\`);
353
+ }
354
+ }
355
+ };
356
+ }
357
+
358
+ writeData(float32Array, trackId = null) {
359
+ let { buffer } = this.write;
360
+ let offset = this.writeOffset;
361
+ for (let i = 0; i < float32Array.length; i++) {
362
+ buffer[offset++] = float32Array[i];
363
+ if (offset >= buffer.length) {
364
+ this.outputBuffers.push(this.write);
365
+ this.write = { buffer: new Float32Array(this.bufferLength), trackId };
366
+ buffer = this.write.buffer;
367
+ offset = 0;
368
+ }
369
+ }
370
+ this.writeOffset = offset;
371
+ return true;
372
+ }
373
+
374
+ process(inputs, outputs, parameters) {
375
+ const output = outputs[0];
376
+ const outputChannelData = output[0];
377
+ const outputBuffers = this.outputBuffers;
378
+ if (this.hasInterrupted) {
379
+ this.port.postMessage({ event: 'stop' });
380
+ return false;
381
+ } else if (outputBuffers.length) {
382
+ this.hasStarted = true;
383
+ const { buffer, trackId } = outputBuffers.shift();
384
+ for (let i = 0; i < outputChannelData.length; i++) {
385
+ outputChannelData[i] = buffer[i] || 0;
386
+ }
387
+ if (trackId) {
388
+ this.trackSampleOffsets[trackId] =
389
+ this.trackSampleOffsets[trackId] || 0;
390
+ this.trackSampleOffsets[trackId] += buffer.length;
391
+ }
392
+ return true;
393
+ } else if (this.hasStarted) {
394
+ this.port.postMessage({ event: 'stop' });
395
+ return false;
396
+ } else {
397
+ return true;
398
+ }
399
+ }
400
+ }
401
+
402
+ registerProcessor('stream_processor', StreamProcessor);
403
+ `;
404
+ const $29a8a70a9466b14f$var$script = new Blob([
405
+ $29a8a70a9466b14f$export$50b76700e2b15e9
406
+ ], {
407
+ type: 'application/javascript'
408
+ });
409
+ const $29a8a70a9466b14f$var$src = URL.createObjectURL($29a8a70a9466b14f$var$script);
410
+ const $29a8a70a9466b14f$export$bfa8c596114d74df = $29a8a70a9466b14f$var$src;
411
+
412
+
413
+
414
+ class $d0a969833958d9e7$export$9698d62c78b8f366 {
415
+ /**
416
+ * Creates a new WavStreamPlayer instance
417
+ * @param {{sampleRate?: number}} options
418
+ * @returns {WavStreamPlayer}
419
+ */ constructor({ sampleRate: sampleRate = 44100 } = {}){
420
+ this.scriptSrc = (0, $29a8a70a9466b14f$export$bfa8c596114d74df);
421
+ this.sampleRate = sampleRate;
422
+ this.context = null;
423
+ this.stream = null;
424
+ this.analyser = null;
425
+ this.trackSampleOffsets = {};
426
+ this.interruptedTrackIds = {};
427
+ }
428
+ /**
429
+ * Connects the audio context and enables output to speakers
430
+ * @returns {Promise<true>}
431
+ */ async connect() {
432
+ this.context = new AudioContext({
433
+ sampleRate: this.sampleRate
434
+ });
435
+ if (this._speakerID) this.context.setSinkId(this._speakerID);
436
+ if (this.context.state === "suspended") await this.context.resume();
437
+ try {
438
+ await this.context.audioWorklet.addModule(this.scriptSrc);
439
+ } catch (e) {
440
+ console.error(e);
441
+ throw new Error(`Could not add audioWorklet module: ${this.scriptSrc}`);
442
+ }
443
+ const analyser = this.context.createAnalyser();
444
+ analyser.fftSize = 8192;
445
+ analyser.smoothingTimeConstant = 0.1;
446
+ this.analyser = analyser;
447
+ return true;
448
+ }
449
+ /**
450
+ * Gets the current frequency domain data from the playing track
451
+ * @param {"frequency"|"music"|"voice"} [analysisType]
452
+ * @param {number} [minDecibels] default -100
453
+ * @param {number} [maxDecibels] default -30
454
+ * @returns {import('./analysis/audio_analysis.js').AudioAnalysisOutputType}
455
+ */ getFrequencies(analysisType = "frequency", minDecibels = -100, maxDecibels = -30) {
456
+ if (!this.analyser) throw new Error("Not connected, please call .connect() first");
457
+ return (0, $f32f064564ee62f6$export$2c3136da0bf130f9).getFrequencies(this.analyser, this.sampleRate, null, analysisType, minDecibels, maxDecibels);
458
+ }
459
+ /**
460
+ * @param {string} speaker deviceId
461
+ */ async updateSpeaker(speaker) {
462
+ const _prevSpeaker = this._speakerID;
463
+ this._speakerID = speaker;
464
+ if (this.context) try {
465
+ if (speaker === "default") await this.context.setSinkId();
466
+ else await this.context.setSinkId(speaker);
467
+ } catch (e) {
468
+ console.error(`Could not set sinkId to ${speaker}: ${e}`);
469
+ this._speakerID = _prevSpeaker;
470
+ }
471
+ }
472
+ /**
473
+ * Starts audio streaming
474
+ * @private
475
+ * @returns {Promise<true>}
476
+ */ _start() {
477
+ const streamNode = new AudioWorkletNode(this.context, "stream_processor");
478
+ streamNode.connect(this.context.destination);
479
+ streamNode.port.onmessage = (e)=>{
480
+ const { event: event } = e.data;
481
+ if (event === "stop") {
482
+ streamNode.disconnect();
483
+ this.stream = null;
484
+ } else if (event === "offset") {
485
+ const { requestId: requestId, trackId: trackId, offset: offset } = e.data;
486
+ const currentTime = offset / this.sampleRate;
487
+ this.trackSampleOffsets[requestId] = {
488
+ trackId: trackId,
489
+ offset: offset,
490
+ currentTime: currentTime
491
+ };
492
+ }
493
+ };
494
+ this.analyser.disconnect();
495
+ streamNode.connect(this.analyser);
496
+ this.stream = streamNode;
497
+ return true;
498
+ }
499
+ /**
500
+ * Adds 16BitPCM data to the currently playing audio stream
501
+ * You can add chunks beyond the current play point and they will be queued for play
502
+ * @param {ArrayBuffer|Int16Array} arrayBuffer
503
+ * @param {string} [trackId]
504
+ * @returns {Int16Array}
505
+ */ add16BitPCM(arrayBuffer, trackId = "default") {
506
+ if (typeof trackId !== "string") throw new Error(`trackId must be a string`);
507
+ else if (this.interruptedTrackIds[trackId]) return;
508
+ if (!this.stream) this._start();
509
+ let buffer;
510
+ if (arrayBuffer instanceof Int16Array) buffer = arrayBuffer;
511
+ else if (arrayBuffer instanceof ArrayBuffer) buffer = new Int16Array(arrayBuffer);
512
+ else throw new Error(`argument must be Int16Array or ArrayBuffer`);
513
+ this.stream.port.postMessage({
514
+ event: "write",
515
+ buffer: buffer,
516
+ trackId: trackId
517
+ });
518
+ return buffer;
519
+ }
520
+ /**
521
+ * Gets the offset (sample count) of the currently playing stream
522
+ * @param {boolean} [interrupt]
523
+ * @returns {{trackId: string|null, offset: number, currentTime: number}}
524
+ */ async getTrackSampleOffset(interrupt = false) {
525
+ if (!this.stream) return null;
526
+ const requestId = crypto.randomUUID();
527
+ this.stream.port.postMessage({
528
+ event: interrupt ? "interrupt" : "offset",
529
+ requestId: requestId
530
+ });
531
+ let trackSampleOffset;
532
+ while(!trackSampleOffset){
533
+ trackSampleOffset = this.trackSampleOffsets[requestId];
534
+ await new Promise((r)=>setTimeout(()=>r(), 1));
535
+ }
536
+ const { trackId: trackId } = trackSampleOffset;
537
+ if (interrupt && trackId) this.interruptedTrackIds[trackId] = true;
538
+ return trackSampleOffset;
539
+ }
540
+ /**
541
+ * Strips the current stream and returns the sample offset of the audio
542
+ * @param {boolean} [interrupt]
543
+ * @returns {{trackId: string|null, offset: number, currentTime: number}}
544
+ */ async interrupt() {
545
+ return this.getTrackSampleOffset(true);
546
+ }
547
+ }
548
+ globalThis.WavStreamPlayer = $d0a969833958d9e7$export$9698d62c78b8f366;
549
+
550
+
551
+ const $8e1d1e6ff08f6fb5$var$AudioProcessorWorklet = `
552
+ class AudioProcessor extends AudioWorkletProcessor {
553
+
554
+ constructor() {
555
+ super();
556
+ this.port.onmessage = this.receive.bind(this);
557
+ this.initialize();
558
+ }
559
+
560
+ initialize() {
561
+ this.foundAudio = false;
562
+ this.recording = false;
563
+ this.chunks = [];
564
+ }
565
+
566
+ /**
567
+ * Concatenates sampled chunks into channels
568
+ * Format is chunk[Left[], Right[]]
569
+ */
570
+ readChannelData(chunks, channel = -1, maxChannels = 9) {
571
+ let channelLimit;
572
+ if (channel !== -1) {
573
+ if (chunks[0] && chunks[0].length - 1 < channel) {
574
+ throw new Error(
575
+ \`Channel \${channel} out of range: max \${chunks[0].length}\`
576
+ );
577
+ }
578
+ channelLimit = channel + 1;
579
+ } else {
580
+ channel = 0;
581
+ channelLimit = Math.min(chunks[0] ? chunks[0].length : 1, maxChannels);
582
+ }
583
+ const channels = [];
584
+ for (let n = channel; n < channelLimit; n++) {
585
+ const length = chunks.reduce((sum, chunk) => {
586
+ return sum + chunk[n].length;
587
+ }, 0);
588
+ const buffers = chunks.map((chunk) => chunk[n]);
589
+ const result = new Float32Array(length);
590
+ let offset = 0;
591
+ for (let i = 0; i < buffers.length; i++) {
592
+ result.set(buffers[i], offset);
593
+ offset += buffers[i].length;
594
+ }
595
+ channels[n] = result;
596
+ }
597
+ return channels;
598
+ }
599
+
600
+ /**
601
+ * Combines parallel audio data into correct format,
602
+ * channels[Left[], Right[]] to float32Array[LRLRLRLR...]
603
+ */
604
+ formatAudioData(channels) {
605
+ if (channels.length === 1) {
606
+ // Simple case is only one channel
607
+ const float32Array = channels[0].slice();
608
+ const meanValues = channels[0].slice();
609
+ return { float32Array, meanValues };
610
+ } else {
611
+ const float32Array = new Float32Array(
612
+ channels[0].length * channels.length
613
+ );
614
+ const meanValues = new Float32Array(channels[0].length);
615
+ for (let i = 0; i < channels[0].length; i++) {
616
+ const offset = i * channels.length;
617
+ let meanValue = 0;
618
+ for (let n = 0; n < channels.length; n++) {
619
+ float32Array[offset + n] = channels[n][i];
620
+ meanValue += channels[n][i];
621
+ }
622
+ meanValues[i] = meanValue / channels.length;
623
+ }
624
+ return { float32Array, meanValues };
625
+ }
626
+ }
627
+
628
+ /**
629
+ * Converts 32-bit float data to 16-bit integers
630
+ */
631
+ floatTo16BitPCM(float32Array) {
632
+ const buffer = new ArrayBuffer(float32Array.length * 2);
633
+ const view = new DataView(buffer);
634
+ let offset = 0;
635
+ for (let i = 0; i < float32Array.length; i++, offset += 2) {
636
+ let s = Math.max(-1, Math.min(1, float32Array[i]));
637
+ view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true);
638
+ }
639
+ return buffer;
640
+ }
641
+
642
+ /**
643
+ * Retrieves the most recent amplitude values from the audio stream
644
+ * @param {number} channel
645
+ */
646
+ getValues(channel = -1) {
647
+ const channels = this.readChannelData(this.chunks, channel);
648
+ const { meanValues } = this.formatAudioData(channels);
649
+ return { meanValues, channels };
650
+ }
651
+
652
+ /**
653
+ * Exports chunks as an audio/wav file
654
+ */
655
+ export() {
656
+ const channels = this.readChannelData(this.chunks);
657
+ const { float32Array, meanValues } = this.formatAudioData(channels);
658
+ const audioData = this.floatTo16BitPCM(float32Array);
659
+ return {
660
+ meanValues: meanValues,
661
+ audio: {
662
+ bitsPerSample: 16,
663
+ channels: channels,
664
+ data: audioData,
665
+ },
666
+ };
667
+ }
668
+
669
+ receive(e) {
670
+ const { event, id } = e.data;
671
+ let receiptData = {};
672
+ switch (event) {
673
+ case 'start':
674
+ this.recording = true;
675
+ break;
676
+ case 'stop':
677
+ this.recording = false;
678
+ break;
679
+ case 'clear':
680
+ this.initialize();
681
+ break;
682
+ case 'export':
683
+ receiptData = this.export();
684
+ break;
685
+ case 'read':
686
+ receiptData = this.getValues();
687
+ break;
688
+ default:
689
+ break;
690
+ }
691
+ // Always send back receipt
692
+ this.port.postMessage({ event: 'receipt', id, data: receiptData });
693
+ }
694
+
695
+ sendChunk(chunk) {
696
+ const channels = this.readChannelData([chunk]);
697
+ const { float32Array, meanValues } = this.formatAudioData(channels);
698
+ const rawAudioData = this.floatTo16BitPCM(float32Array);
699
+ const monoAudioData = this.floatTo16BitPCM(meanValues);
700
+ this.port.postMessage({
701
+ event: 'chunk',
702
+ data: {
703
+ mono: monoAudioData,
704
+ raw: rawAudioData,
705
+ },
706
+ });
707
+ }
708
+
709
+ process(inputList, outputList, parameters) {
710
+ // Copy input to output (e.g. speakers)
711
+ // Note that this creates choppy sounds with Mac products
712
+ const sourceLimit = Math.min(inputList.length, outputList.length);
713
+ for (let inputNum = 0; inputNum < sourceLimit; inputNum++) {
714
+ const input = inputList[inputNum];
715
+ const output = outputList[inputNum];
716
+ const channelCount = Math.min(input.length, output.length);
717
+ for (let channelNum = 0; channelNum < channelCount; channelNum++) {
718
+ input[channelNum].forEach((sample, i) => {
719
+ output[channelNum][i] = sample;
720
+ });
721
+ }
722
+ }
723
+ const inputs = inputList[0];
724
+ // There's latency at the beginning of a stream before recording starts
725
+ // Make sure we actually receive audio data before we start storing chunks
726
+ let sliceIndex = 0;
727
+ if (!this.foundAudio) {
728
+ for (const channel of inputs) {
729
+ sliceIndex = 0; // reset for each channel
730
+ if (this.foundAudio) {
731
+ break;
732
+ }
733
+ if (channel) {
734
+ for (const value of channel) {
735
+ if (value !== 0) {
736
+ // find only one non-zero entry in any channel
737
+ this.foundAudio = true;
738
+ break;
739
+ } else {
740
+ sliceIndex++;
741
+ }
742
+ }
743
+ }
744
+ }
745
+ }
746
+ if (inputs && inputs[0] && this.foundAudio && this.recording) {
747
+ // We need to copy the TypedArray, because the \`process\`
748
+ // internals will reuse the same buffer to hold each input
749
+ const chunk = inputs.map((input) => input.slice(sliceIndex));
750
+ this.chunks.push(chunk);
751
+ this.sendChunk(chunk);
752
+ }
753
+ return true;
754
+ }
755
+ }
756
+
757
+ registerProcessor('audio_processor', AudioProcessor);
758
+ `;
759
+ const $8e1d1e6ff08f6fb5$var$script = new Blob([
760
+ $8e1d1e6ff08f6fb5$var$AudioProcessorWorklet
761
+ ], {
762
+ type: 'application/javascript'
763
+ });
764
+ const $8e1d1e6ff08f6fb5$var$src = URL.createObjectURL($8e1d1e6ff08f6fb5$var$script);
765
+ const $8e1d1e6ff08f6fb5$export$1f65f50a8cbff43c = $8e1d1e6ff08f6fb5$var$src;
766
+
767
+
768
+
769
+
770
+ class $62bc376044a05513$export$439b217ca659a877 {
771
+ /**
772
+ * Create a new WavRecorder instance
773
+ * @param {{sampleRate?: number, outputToSpeakers?: boolean, debug?: boolean}} [options]
774
+ * @returns {WavRecorder}
775
+ */ constructor({ sampleRate: sampleRate = 44100, outputToSpeakers: outputToSpeakers = false, debug: debug = false } = {}){
776
+ // Script source
777
+ this.scriptSrc = (0, $8e1d1e6ff08f6fb5$export$1f65f50a8cbff43c);
778
+ // Config
779
+ this.sampleRate = sampleRate;
780
+ this.outputToSpeakers = outputToSpeakers;
781
+ this.debug = !!debug;
782
+ this._deviceChangeCallback = null;
783
+ this._devices = [];
784
+ this.deviceSelection = null;
785
+ // State variables
786
+ this.stream = null;
787
+ this.processor = null;
788
+ this.source = null;
789
+ this.node = null;
790
+ this.recording = false;
791
+ // Event handling with AudioWorklet
792
+ this._lastEventId = 0;
793
+ this.eventReceipts = {};
794
+ this.eventTimeout = 5000;
795
+ // Process chunks of audio
796
+ this._chunkProcessor = ()=>{};
797
+ this._chunkProcessorSize = void 0;
798
+ this._chunkProcessorBuffer = {
799
+ raw: new ArrayBuffer(0),
800
+ mono: new ArrayBuffer(0)
801
+ };
802
+ }
803
+ /**
804
+ * Decodes audio data from multiple formats to a Blob, url, Float32Array and AudioBuffer
805
+ * @param {Blob|Float32Array|Int16Array|ArrayBuffer|number[]} audioData
806
+ * @param {number} sampleRate
807
+ * @param {number} fromSampleRate
808
+ * @returns {Promise<DecodedAudioType>}
809
+ */ static async decode(audioData, sampleRate = 44100, fromSampleRate = -1) {
810
+ const context = new AudioContext({
811
+ sampleRate: sampleRate
812
+ });
813
+ let arrayBuffer;
814
+ let blob;
815
+ if (audioData instanceof Blob) {
816
+ if (fromSampleRate !== -1) throw new Error(`Can not specify "fromSampleRate" when reading from Blob`);
817
+ blob = audioData;
818
+ arrayBuffer = await blob.arrayBuffer();
819
+ } else if (audioData instanceof ArrayBuffer) {
820
+ if (fromSampleRate !== -1) throw new Error(`Can not specify "fromSampleRate" when reading from ArrayBuffer`);
821
+ arrayBuffer = audioData;
822
+ blob = new Blob([
823
+ arrayBuffer
824
+ ], {
825
+ type: 'audio/wav'
826
+ });
827
+ } else {
828
+ let float32Array;
829
+ let data;
830
+ if (audioData instanceof Int16Array) {
831
+ data = audioData;
832
+ float32Array = new Float32Array(audioData.length);
833
+ for(let i = 0; i < audioData.length; i++)float32Array[i] = audioData[i] / 0x8000;
834
+ } else if (audioData instanceof Float32Array) float32Array = audioData;
835
+ else if (audioData instanceof Array) float32Array = new Float32Array(audioData);
836
+ else throw new Error(`"audioData" must be one of: Blob, Float32Arrray, Int16Array, ArrayBuffer, Array<number>`);
837
+ if (fromSampleRate === -1) throw new Error(`Must specify "fromSampleRate" when reading from Float32Array, In16Array or Array`);
838
+ else if (fromSampleRate < 3000) throw new Error(`Minimum "fromSampleRate" is 3000 (3kHz)`);
839
+ if (!data) data = (0, $6d4b7449a1e1544a$export$13afda237b1c9846).floatTo16BitPCM(float32Array);
840
+ const audio = {
841
+ bitsPerSample: 16,
842
+ channels: [
843
+ float32Array
844
+ ],
845
+ data: data
846
+ };
847
+ const packer = new (0, $6d4b7449a1e1544a$export$13afda237b1c9846)();
848
+ const result = packer.pack(fromSampleRate, audio);
849
+ blob = result.blob;
850
+ arrayBuffer = await blob.arrayBuffer();
851
+ }
852
+ const audioBuffer = await context.decodeAudioData(arrayBuffer);
853
+ const values = audioBuffer.getChannelData(0);
854
+ const url = URL.createObjectURL(blob);
855
+ return {
856
+ blob: blob,
857
+ url: url,
858
+ values: values,
859
+ audioBuffer: audioBuffer
860
+ };
861
+ }
862
+ /**
863
+ * Logs data in debug mode
864
+ * @param {...any} arguments
865
+ * @returns {true}
866
+ */ log() {
867
+ if (this.debug) this.log(...arguments);
868
+ return true;
869
+ }
870
+ /**
871
+ * Retrieves the current sampleRate for the recorder
872
+ * @returns {number}
873
+ */ getSampleRate() {
874
+ return this.sampleRate;
875
+ }
876
+ /**
877
+ * Retrieves the current status of the recording
878
+ * @returns {"ended"|"paused"|"recording"}
879
+ */ getStatus() {
880
+ if (!this.processor) return 'ended';
881
+ else if (!this.recording) return 'paused';
882
+ else return 'recording';
883
+ }
884
+ /**
885
+ * Sends an event to the AudioWorklet
886
+ * @private
887
+ * @param {string} name
888
+ * @param {{[key: string]: any}} data
889
+ * @param {AudioWorkletNode} [_processor]
890
+ * @returns {Promise<{[key: string]: any}>}
891
+ */ async _event(name, data = {}, _processor = null) {
892
+ _processor = _processor || this.processor;
893
+ if (!_processor) throw new Error('Can not send events without recording first');
894
+ const message = {
895
+ event: name,
896
+ id: this._lastEventId++,
897
+ data: data
898
+ };
899
+ _processor.port.postMessage(message);
900
+ const t0 = new Date().valueOf();
901
+ while(!this.eventReceipts[message.id]){
902
+ if (new Date().valueOf() - t0 > this.eventTimeout) throw new Error(`Timeout waiting for "${name}" event`);
903
+ await new Promise((res)=>setTimeout(()=>res(true), 1));
904
+ }
905
+ const payload = this.eventReceipts[message.id];
906
+ delete this.eventReceipts[message.id];
907
+ return payload;
908
+ }
909
+ /**
910
+ * Sets device change callback, remove if callback provided is `null`
911
+ * @param {(Array<MediaDeviceInfo & {default: boolean}>): void|null} callback
912
+ * @returns {true}
913
+ */ listenForDeviceChange(callback) {
914
+ if (callback === null && this._deviceChangeCallback) {
915
+ navigator.mediaDevices.removeEventListener('devicechange', this._deviceChangeCallback);
916
+ this._deviceChangeCallback = null;
917
+ } else if (callback !== null) {
918
+ // Basically a debounce; we only want this called once when devices change
919
+ // And we only want the most recent callback() to be executed
920
+ // if a few are operating at the same time
921
+ let lastId = 0;
922
+ let lastDevices = [];
923
+ const serializeDevices = (devices)=>devices.map((d)=>d.deviceId).sort().join(',');
924
+ const cb = async ()=>{
925
+ let id = ++lastId;
926
+ const devices = await this.listDevices();
927
+ if (id === lastId) {
928
+ if (serializeDevices(lastDevices) !== serializeDevices(devices)) {
929
+ lastDevices = devices;
930
+ callback(devices.slice());
931
+ }
932
+ }
933
+ };
934
+ navigator.mediaDevices.addEventListener('devicechange', cb);
935
+ cb();
936
+ this._deviceChangeCallback = cb;
937
+ }
938
+ return true;
939
+ }
940
+ /**
941
+ * Manually request permission to use the microphone
942
+ * @returns {Promise<true>}
943
+ */ async requestPermission() {
944
+ const permissionStatus = await navigator.permissions.query({
945
+ name: 'microphone'
946
+ });
947
+ if (permissionStatus.state === 'denied') window.alert('You must grant microphone access to use this feature.');
948
+ else if (permissionStatus.state === 'prompt') try {
949
+ const stream = await navigator.mediaDevices.getUserMedia({
950
+ audio: true
951
+ });
952
+ const tracks = stream.getTracks();
953
+ tracks.forEach((track)=>track.stop());
954
+ } catch (e) {
955
+ window.alert('You must grant microphone access to use this feature.');
956
+ }
957
+ return true;
958
+ }
959
+ /**
960
+ * List all eligible devices for recording, will request permission to use microphone
961
+ * @returns {Promise<Array<MediaDeviceInfo & {default: boolean}>>}
962
+ */ async listDevices() {
963
+ if (!navigator.mediaDevices || !('enumerateDevices' in navigator.mediaDevices)) throw new Error('Could not request user devices');
964
+ await this.requestPermission();
965
+ const devices = await navigator.mediaDevices.enumerateDevices();
966
+ const audioDevices = devices.filter((device)=>device.kind === 'audioinput');
967
+ return audioDevices;
968
+ // const defaultDeviceIndex = audioDevices.findIndex(
969
+ // (device) => device.deviceId === 'default'
970
+ // );
971
+ // const deviceList = [];
972
+ // if (defaultDeviceIndex !== -1) {
973
+ // let defaultDevice = audioDevices.splice(defaultDeviceIndex, 1)[0];
974
+ // let existingIndex = audioDevices.findIndex(
975
+ // (device) => device.groupId === defaultDevice.groupId
976
+ // );
977
+ // if (existingIndex !== -1) {
978
+ // defaultDevice = audioDevices.splice(existingIndex, 1)[0];
979
+ // }
980
+ // defaultDevice.default = true;
981
+ // deviceList.push(defaultDevice);
982
+ // }
983
+ // return deviceList.concat(audioDevices);
984
+ }
985
+ /**
986
+ * Begins a recording session and requests microphone permissions if not already granted
987
+ * Microphone recording indicator will appear on browser tab but status will be "paused"
988
+ * @param {string} [deviceId] if no device provided, default device will be used
989
+ * @returns {Promise<true>}
990
+ */ async begin(deviceId) {
991
+ if (this.processor) throw new Error(`Already connected: please call .end() to start a new session`);
992
+ if (!navigator.mediaDevices || !('getUserMedia' in navigator.mediaDevices)) throw new Error('Could not request user media');
993
+ deviceId = deviceId ?? this.deviceSelection?.deviceId;
994
+ try {
995
+ const config = {
996
+ audio: true
997
+ };
998
+ if (deviceId) config.audio = {
999
+ deviceId: {
1000
+ exact: deviceId
1001
+ }
1002
+ };
1003
+ this.stream = await navigator.mediaDevices.getUserMedia(config);
1004
+ } catch (err) {
1005
+ throw new Error('Could not start media stream');
1006
+ }
1007
+ this.listDevices().then((devices)=>{
1008
+ deviceId = this.stream.getAudioTracks()[0].getSettings().deviceId;
1009
+ console.log('find current device', devices, deviceId, this.stream.getAudioTracks()[0].getSettings());
1010
+ this.deviceSelection = devices.find((d)=>d.deviceId === deviceId);
1011
+ console.log('current device', this.deviceSelection);
1012
+ });
1013
+ const context = new AudioContext({
1014
+ sampleRate: this.sampleRate
1015
+ });
1016
+ const source = context.createMediaStreamSource(this.stream);
1017
+ // Load and execute the module script.
1018
+ try {
1019
+ await context.audioWorklet.addModule(this.scriptSrc);
1020
+ } catch (e) {
1021
+ console.error(e);
1022
+ throw new Error(`Could not add audioWorklet module: ${this.scriptSrc}`);
1023
+ }
1024
+ const processor = new AudioWorkletNode(context, 'audio_processor');
1025
+ processor.port.onmessage = (e)=>{
1026
+ const { event: event, id: id, data: data } = e.data;
1027
+ if (event === 'receipt') this.eventReceipts[id] = data;
1028
+ else if (event === 'chunk') {
1029
+ if (this._chunkProcessorSize) {
1030
+ const buffer = this._chunkProcessorBuffer;
1031
+ this._chunkProcessorBuffer = {
1032
+ raw: (0, $6d4b7449a1e1544a$export$13afda237b1c9846).mergeBuffers(buffer.raw, data.raw),
1033
+ mono: (0, $6d4b7449a1e1544a$export$13afda237b1c9846).mergeBuffers(buffer.mono, data.mono)
1034
+ };
1035
+ if (this._chunkProcessorBuffer.mono.byteLength >= this._chunkProcessorSize) {
1036
+ this._chunkProcessor(this._chunkProcessorBuffer);
1037
+ this._chunkProcessorBuffer = {
1038
+ raw: new ArrayBuffer(0),
1039
+ mono: new ArrayBuffer(0)
1040
+ };
1041
+ }
1042
+ } else this._chunkProcessor(data);
1043
+ }
1044
+ };
1045
+ const node = source.connect(processor);
1046
+ const analyser = context.createAnalyser();
1047
+ analyser.fftSize = 8192;
1048
+ analyser.smoothingTimeConstant = 0.1;
1049
+ node.connect(analyser);
1050
+ if (this.outputToSpeakers) {
1051
+ // eslint-disable-next-line no-console
1052
+ console.warn("Warning: Output to speakers may affect sound quality,\nespecially due to system audio feedback preventative measures.\nuse only for debugging");
1053
+ analyser.connect(context.destination);
1054
+ }
1055
+ this.source = source;
1056
+ this.node = node;
1057
+ this.analyser = analyser;
1058
+ this.processor = processor;
1059
+ console.log('begin completed');
1060
+ return true;
1061
+ }
1062
+ /**
1063
+ * Gets the current frequency domain data from the recording track
1064
+ * @param {"frequency"|"music"|"voice"} [analysisType]
1065
+ * @param {number} [minDecibels] default -100
1066
+ * @param {number} [maxDecibels] default -30
1067
+ * @returns {import('./analysis/audio_analysis.js').AudioAnalysisOutputType}
1068
+ */ getFrequencies(analysisType = 'frequency', minDecibels = -100, maxDecibels = -30) {
1069
+ if (!this.processor) throw new Error('Session ended: please call .begin() first');
1070
+ return (0, $f32f064564ee62f6$export$2c3136da0bf130f9).getFrequencies(this.analyser, this.sampleRate, null, analysisType, minDecibels, maxDecibels);
1071
+ }
1072
+ /**
1073
+ * Pauses the recording
1074
+ * Keeps microphone stream open but halts storage of audio
1075
+ * @returns {Promise<true>}
1076
+ */ async pause() {
1077
+ if (!this.processor) throw new Error('Session ended: please call .begin() first');
1078
+ else if (!this.recording) throw new Error('Already paused: please call .record() first');
1079
+ if (this._chunkProcessorBuffer.raw.byteLength) this._chunkProcessor(this._chunkProcessorBuffer);
1080
+ this.log('Pausing ...');
1081
+ await this._event('stop');
1082
+ this.recording = false;
1083
+ return true;
1084
+ }
1085
+ /**
1086
+ * Start recording stream and storing to memory from the connected audio source
1087
+ * @param {(data: { mono: Int16Array; raw: Int16Array }) => any} [chunkProcessor]
1088
+ * @param {number} [chunkSize] chunkProcessor will not be triggered until this size threshold met in mono audio
1089
+ * @returns {Promise<true>}
1090
+ */ async record(chunkProcessor = ()=>{}, chunkSize = 8192) {
1091
+ if (!this.processor) throw new Error('Session ended: please call .begin() first');
1092
+ else if (this.recording) throw new Error('Already recording: please call .pause() first');
1093
+ else if (typeof chunkProcessor !== 'function') throw new Error(`chunkProcessor must be a function`);
1094
+ this._chunkProcessor = chunkProcessor;
1095
+ this._chunkProcessorSize = chunkSize;
1096
+ this._chunkProcessorBuffer = {
1097
+ raw: new ArrayBuffer(0),
1098
+ mono: new ArrayBuffer(0)
1099
+ };
1100
+ this.log('Recording ...');
1101
+ await this._event('start');
1102
+ this.recording = true;
1103
+ return true;
1104
+ }
1105
+ /**
1106
+ * Clears the audio buffer, empties stored recording
1107
+ * @returns {Promise<true>}
1108
+ */ async clear() {
1109
+ if (!this.processor) throw new Error('Session ended: please call .begin() first');
1110
+ await this._event('clear');
1111
+ return true;
1112
+ }
1113
+ /**
1114
+ * Reads the current audio stream data
1115
+ * @returns {Promise<{meanValues: Float32Array, channels: Array<Float32Array>}>}
1116
+ */ async read() {
1117
+ if (!this.processor) throw new Error('Session ended: please call .begin() first');
1118
+ this.log('Reading ...');
1119
+ const result = await this._event('read');
1120
+ return result;
1121
+ }
1122
+ /**
1123
+ * Saves the current audio stream to a file
1124
+ * @param {boolean} [force] Force saving while still recording
1125
+ * @returns {Promise<import('./wav_packer.js').WavPackerAudioType>}
1126
+ */ async save(force = false) {
1127
+ if (!this.processor) throw new Error('Session ended: please call .begin() first');
1128
+ if (!force && this.recording) throw new Error('Currently recording: please call .pause() first, or call .save(true) to force');
1129
+ this.log('Exporting ...');
1130
+ const exportData = await this._event('export');
1131
+ const packer = new (0, $6d4b7449a1e1544a$export$13afda237b1c9846)();
1132
+ const result = packer.pack(this.sampleRate, exportData.audio);
1133
+ return result;
1134
+ }
1135
+ /**
1136
+ * Ends the current recording session and saves the result
1137
+ * @returns {Promise<import('./wav_packer.js').WavPackerAudioType>}
1138
+ */ async end() {
1139
+ if (!this.processor) throw new Error('Session ended: please call .begin() first');
1140
+ const _processor = this.processor;
1141
+ this.log('Stopping ...');
1142
+ await this._event('stop');
1143
+ this.recording = false;
1144
+ const tracks = this.stream.getTracks();
1145
+ tracks.forEach((track)=>track.stop());
1146
+ this.log('Exporting ...');
1147
+ const exportData = await this._event('export', {}, _processor);
1148
+ this.processor.disconnect();
1149
+ this.source.disconnect();
1150
+ this.node.disconnect();
1151
+ this.analyser.disconnect();
1152
+ this.stream = null;
1153
+ this.processor = null;
1154
+ this.source = null;
1155
+ this.node = null;
1156
+ const packer = new (0, $6d4b7449a1e1544a$export$13afda237b1c9846)();
1157
+ const result = packer.pack(this.sampleRate, exportData.audio);
1158
+ return result;
1159
+ }
1160
+ /**
1161
+ * Performs a full cleanup of WavRecorder instance
1162
+ * Stops actively listening via microphone and removes existing listeners
1163
+ * @returns {Promise<true>}
1164
+ */ async quit() {
1165
+ this.listenForDeviceChange(null);
1166
+ // we do not reset this on end so that selections persist across starts
1167
+ this.deviceSelection = null;
1168
+ if (this.processor) await this.end();
1169
+ return true;
1170
+ }
1171
+ }
1172
+ globalThis.WavRecorder = $62bc376044a05513$export$439b217ca659a877;
1173
+
1174
+
1175
+
1176
+
1177
+
1178
+ class $5fc11d7bc0d20724$export$2934cf2d25c67a48 {
1179
+ /**
1180
+ * Create a new MediaStreamRecorder instance
1181
+ * @param {{sampleRate?: number, outputToSpeakers?: boolean, debug?: boolean}} [options]
1182
+ * @returns {MediaStreamRecorder}
1183
+ */ constructor({ sampleRate: sampleRate = 44100, outputToSpeakers: outputToSpeakers = false, debug: debug = false } = {}){
1184
+ // Script source
1185
+ this.scriptSrc = (0, $8e1d1e6ff08f6fb5$export$1f65f50a8cbff43c);
1186
+ // Config
1187
+ this.sampleRate = sampleRate;
1188
+ this.outputToSpeakers = outputToSpeakers;
1189
+ this.debug = !!debug;
1190
+ // State variables
1191
+ this.stream = null;
1192
+ this.processor = null;
1193
+ this.source = null;
1194
+ this.node = null;
1195
+ this.recording = false;
1196
+ // Event handling with AudioWorklet
1197
+ this._lastEventId = 0;
1198
+ this.eventReceipts = {};
1199
+ this.eventTimeout = 5000;
1200
+ // Process chunks of audio
1201
+ this._chunkProcessor = ()=>{};
1202
+ this._chunkProcessorSize = void 0;
1203
+ this._chunkProcessorBuffer = {
1204
+ raw: new ArrayBuffer(0),
1205
+ mono: new ArrayBuffer(0)
1206
+ };
1207
+ }
1208
+ /**
1209
+ * Logs data in debug mode
1210
+ * @param {...any} arguments
1211
+ * @returns {true}
1212
+ */ log() {
1213
+ if (this.debug) this.log(...arguments);
1214
+ return true;
1215
+ }
1216
+ /**
1217
+ * Retrieves the current sampleRate for the recorder
1218
+ * @returns {number}
1219
+ */ getSampleRate() {
1220
+ return this.sampleRate;
1221
+ }
1222
+ /**
1223
+ * Retrieves the current status of the recording
1224
+ * @returns {"ended"|"paused"|"recording"}
1225
+ */ getStatus() {
1226
+ if (!this.processor) return "ended";
1227
+ else if (!this.recording) return "paused";
1228
+ else return "recording";
1229
+ }
1230
+ /**
1231
+ * Sends an event to the AudioWorklet
1232
+ * @private
1233
+ * @param {string} name
1234
+ * @param {{[key: string]: any}} data
1235
+ * @param {AudioWorkletNode} [_processor]
1236
+ * @returns {Promise<{[key: string]: any}>}
1237
+ */ async _event(name, data = {}, _processor = null) {
1238
+ _processor = _processor || this.processor;
1239
+ if (!_processor) throw new Error("Can not send events without recording first");
1240
+ const message = {
1241
+ event: name,
1242
+ id: this._lastEventId++,
1243
+ data: data
1244
+ };
1245
+ _processor.port.postMessage(message);
1246
+ const t0 = new Date().valueOf();
1247
+ while(!this.eventReceipts[message.id]){
1248
+ if (new Date().valueOf() - t0 > this.eventTimeout) throw new Error(`Timeout waiting for "${name}" event`);
1249
+ await new Promise((res)=>setTimeout(()=>res(true), 1));
1250
+ }
1251
+ const payload = this.eventReceipts[message.id];
1252
+ delete this.eventReceipts[message.id];
1253
+ return payload;
1254
+ }
1255
+ /**
1256
+ * Begins a recording session for the given audioTrack
1257
+ * Microphone recording indicator will appear on browser tab but status will be "paused"
1258
+ * @param {MediaStreamTrack} [audioTrack] if no device provided, default device will be used
1259
+ * @returns {Promise<true>}
1260
+ */ async begin(audioTrack) {
1261
+ if (this.processor) throw new Error(`Already connected: please call .end() to start a new session`);
1262
+ if (!audioTrack || audioTrack.kind !== "audio") throw new Error("No audio track provided");
1263
+ this.stream = new MediaStream([
1264
+ audioTrack
1265
+ ]);
1266
+ const context = new AudioContext({
1267
+ sampleRate: this.sampleRate
1268
+ });
1269
+ const source = context.createMediaStreamSource(this.stream);
1270
+ // Load and execute the module script.
1271
+ try {
1272
+ await context.audioWorklet.addModule(this.scriptSrc);
1273
+ } catch (e) {
1274
+ console.error(e);
1275
+ throw new Error(`Could not add audioWorklet module: ${this.scriptSrc}`);
1276
+ }
1277
+ const processor = new AudioWorkletNode(context, "audio_processor");
1278
+ processor.port.onmessage = (e)=>{
1279
+ const { event: event, id: id, data: data } = e.data;
1280
+ if (event === "receipt") this.eventReceipts[id] = data;
1281
+ else if (event === "chunk") {
1282
+ if (this._chunkProcessorSize) {
1283
+ const buffer = this._chunkProcessorBuffer;
1284
+ this._chunkProcessorBuffer = {
1285
+ raw: (0, $6d4b7449a1e1544a$export$13afda237b1c9846).mergeBuffers(buffer.raw, data.raw),
1286
+ mono: (0, $6d4b7449a1e1544a$export$13afda237b1c9846).mergeBuffers(buffer.mono, data.mono)
1287
+ };
1288
+ if (this._chunkProcessorBuffer.mono.byteLength >= this._chunkProcessorSize) {
1289
+ this._chunkProcessor(this._chunkProcessorBuffer);
1290
+ this._chunkProcessorBuffer = {
1291
+ raw: new ArrayBuffer(0),
1292
+ mono: new ArrayBuffer(0)
1293
+ };
1294
+ }
1295
+ } else this._chunkProcessor(data);
1296
+ }
1297
+ };
1298
+ const node = source.connect(processor);
1299
+ const analyser = context.createAnalyser();
1300
+ analyser.fftSize = 8192;
1301
+ analyser.smoothingTimeConstant = 0.1;
1302
+ node.connect(analyser);
1303
+ if (this.outputToSpeakers) {
1304
+ // eslint-disable-next-line no-console
1305
+ console.warn("Warning: Output to speakers may affect sound quality,\nespecially due to system audio feedback preventative measures.\nuse only for debugging");
1306
+ analyser.connect(context.destination);
1307
+ }
1308
+ this.source = source;
1309
+ this.node = node;
1310
+ this.analyser = analyser;
1311
+ this.processor = processor;
1312
+ return true;
1313
+ }
1314
+ /**
1315
+ * Gets the current frequency domain data from the recording track
1316
+ * @param {"frequency"|"music"|"voice"} [analysisType]
1317
+ * @param {number} [minDecibels] default -100
1318
+ * @param {number} [maxDecibels] default -30
1319
+ * @returns {import('./analysis/audio_analysis.js').AudioAnalysisOutputType}
1320
+ */ getFrequencies(analysisType = "frequency", minDecibels = -100, maxDecibels = -30) {
1321
+ if (!this.processor) throw new Error("Session ended: please call .begin() first");
1322
+ return (0, $f32f064564ee62f6$export$2c3136da0bf130f9).getFrequencies(this.analyser, this.sampleRate, null, analysisType, minDecibels, maxDecibels);
1323
+ }
1324
+ /**
1325
+ * Pauses the recording
1326
+ * Keeps microphone stream open but halts storage of audio
1327
+ * @returns {Promise<true>}
1328
+ */ async pause() {
1329
+ if (!this.processor) throw new Error("Session ended: please call .begin() first");
1330
+ else if (!this.recording) throw new Error("Already paused: please call .record() first");
1331
+ if (this._chunkProcessorBuffer.raw.byteLength) this._chunkProcessor(this._chunkProcessorBuffer);
1332
+ this.log("Pausing ...");
1333
+ await this._event("stop");
1334
+ this.recording = false;
1335
+ return true;
1336
+ }
1337
+ /**
1338
+ * Start recording stream and storing to memory from the connected audio source
1339
+ * @param {(data: { mono: Int16Array; raw: Int16Array }) => any} [chunkProcessor]
1340
+ * @param {number} [chunkSize] chunkProcessor will not be triggered until this size threshold met in mono audio
1341
+ * @returns {Promise<true>}
1342
+ */ async record(chunkProcessor = ()=>{}, chunkSize = 8192) {
1343
+ if (!this.processor) throw new Error("Session ended: please call .begin() first");
1344
+ else if (this.recording) throw new Error("Already recording: HELLO please call .pause() first");
1345
+ else if (typeof chunkProcessor !== "function") throw new Error(`chunkProcessor must be a function`);
1346
+ this._chunkProcessor = chunkProcessor;
1347
+ this._chunkProcessorSize = chunkSize;
1348
+ this._chunkProcessorBuffer = {
1349
+ raw: new ArrayBuffer(0),
1350
+ mono: new ArrayBuffer(0)
1351
+ };
1352
+ this.log("Recording ...");
1353
+ await this._event("start");
1354
+ this.recording = true;
1355
+ return true;
1356
+ }
1357
+ /**
1358
+ * Clears the audio buffer, empties stored recording
1359
+ * @returns {Promise<true>}
1360
+ */ async clear() {
1361
+ if (!this.processor) throw new Error("Session ended: please call .begin() first");
1362
+ await this._event("clear");
1363
+ return true;
1364
+ }
1365
+ /**
1366
+ * Reads the current audio stream data
1367
+ * @returns {Promise<{meanValues: Float32Array, channels: Array<Float32Array>}>}
1368
+ */ async read() {
1369
+ if (!this.processor) throw new Error("Session ended: please call .begin() first");
1370
+ this.log("Reading ...");
1371
+ const result = await this._event("read");
1372
+ return result;
1373
+ }
1374
+ /**
1375
+ * Saves the current audio stream to a file
1376
+ * @param {boolean} [force] Force saving while still recording
1377
+ * @returns {Promise<import('./wav_packer.js').WavPackerAudioType>}
1378
+ */ async save(force = false) {
1379
+ if (!this.processor) throw new Error("Session ended: please call .begin() first");
1380
+ if (!force && this.recording) throw new Error("Currently recording: please call .pause() first, or call .save(true) to force");
1381
+ this.log("Exporting ...");
1382
+ const exportData = await this._event("export");
1383
+ const packer = new (0, $6d4b7449a1e1544a$export$13afda237b1c9846)();
1384
+ const result = packer.pack(this.sampleRate, exportData.audio);
1385
+ return result;
1386
+ }
1387
+ /**
1388
+ * Ends the current recording session and saves the result
1389
+ * @returns {Promise<import('./wav_packer.js').WavPackerAudioType>}
1390
+ */ async end() {
1391
+ if (!this.processor) throw new Error("Session ended: please call .begin() first");
1392
+ const _processor = this.processor;
1393
+ this.log("Stopping ...");
1394
+ await this._event("stop");
1395
+ this.recording = false;
1396
+ this.log("Exporting ...");
1397
+ const exportData = await this._event("export", {}, _processor);
1398
+ this.processor.disconnect();
1399
+ this.source.disconnect();
1400
+ this.node.disconnect();
1401
+ this.analyser.disconnect();
1402
+ this.stream = null;
1403
+ this.processor = null;
1404
+ this.source = null;
1405
+ this.node = null;
1406
+ const packer = new (0, $6d4b7449a1e1544a$export$13afda237b1c9846)();
1407
+ const result = packer.pack(this.sampleRate, exportData.audio);
1408
+ return result;
1409
+ }
1410
+ /**
1411
+ * Performs a full cleanup of WavRecorder instance
1412
+ * Stops actively listening via microphone and removes existing listeners
1413
+ * @returns {Promise<true>}
1414
+ */ async quit() {
1415
+ this.listenForDeviceChange(null);
1416
+ if (this.processor) await this.end();
1417
+ return true;
1418
+ }
1419
+ }
1420
+ globalThis.WavRecorder = WavRecorder;
1421
+
1422
+
1423
+
1424
+
1425
+ var $683f111f61e07358$export$ef180de88fd317cc;
1426
+ (function(DailyRTVIMessageType) {
1427
+ DailyRTVIMessageType["AUDIO_BUFFERING_STARTED"] = "audio-buffering-started";
1428
+ DailyRTVIMessageType["AUDIO_BUFFERING_STOPPED"] = "audio-buffering-stopped";
1429
+ })($683f111f61e07358$export$ef180de88fd317cc || ($683f111f61e07358$export$ef180de88fd317cc = {}));
1430
+ class $683f111f61e07358$var$DailyCallWrapper {
1431
+ constructor(daily){
1432
+ this._daily = daily;
1433
+ this._proxy = new Proxy(this._daily, {
1434
+ get: (target, prop, receiver)=>{
1435
+ if (typeof target[prop] === "function") {
1436
+ let errMsg;
1437
+ switch(String(prop)){
1438
+ // Disable methods that modify the lifecycle of the call. These operations
1439
+ // should be performed via the RTVI client in order to keep state in sync.
1440
+ case "preAuth":
1441
+ errMsg = `Calls to preAuth() are disabled. Please use Transport.preAuth()`;
1442
+ break;
1443
+ case "startCamera":
1444
+ errMsg = `Calls to startCamera() are disabled. Please use RTVIClient.initDevices() or Transport.initDevices()`;
1445
+ break;
1446
+ case "join":
1447
+ errMsg = `Calls to join() are disabled. Please use RTVIClient.connect()`;
1448
+ break;
1449
+ case "leave":
1450
+ errMsg = `Calls to leave() are disabled. Please use RTVIClient.disconnect()`;
1451
+ break;
1452
+ case "destroy":
1453
+ errMsg = `Calls to destroy() are disabled.`;
1454
+ break;
1455
+ }
1456
+ if (errMsg) return ()=>{
1457
+ throw new Error(errMsg);
1458
+ };
1459
+ // Forward other method calls
1460
+ return (...args)=>{
1461
+ return target[prop](...args);
1462
+ };
1463
+ }
1464
+ // Forward property access
1465
+ return Reflect.get(target, prop, receiver);
1466
+ }
1467
+ });
1468
+ }
1469
+ get proxy() {
1470
+ return this._proxy;
1471
+ }
1472
+ }
13
1473
  class $683f111f61e07358$export$b1ca982aa1e488c1 extends (0, $7zbD8$Transport) {
14
- constructor({ dailyFactoryOptions: dailyFactoryOptions = {} } = {}){
1474
+ constructor({ dailyFactoryOptions: dailyFactoryOptions = {}, bufferLocalAudioUntilBotReady: bufferLocalAudioUntilBotReady = false } = {}){
15
1475
  super();
16
1476
  this._botId = "";
17
1477
  this._selectedCam = {};
18
1478
  this._selectedMic = {};
19
1479
  this._selectedSpeaker = {};
1480
+ this._currentAudioTrack = null;
1481
+ this._audioQueue = [];
1482
+ this._callbacks = {};
20
1483
  this._dailyFactoryOptions = dailyFactoryOptions;
21
- }
22
- initialize(options, messageHandler) {
23
- this._callbacks = options.callbacks ?? {};
24
- this._onMessage = messageHandler;
25
- const existingInstance = (0, $7zbD8$dailycodailyjs).getCallInstance();
26
- if (existingInstance) existingInstance.destroy();
1484
+ this._bufferLocalAudioUntilBotReady = bufferLocalAudioUntilBotReady;
27
1485
  this._daily = (0, $7zbD8$dailycodailyjs).createCallObject({
28
1486
  ...this._dailyFactoryOptions,
29
- // Default is cam off
30
- startVideoOff: options.enableCam != true,
31
- // Default is mic on
32
- startAudioOff: options.enableMic == false,
33
1487
  allowMultipleCallInstances: true
34
1488
  });
1489
+ this._dailyWrapper = new $683f111f61e07358$var$DailyCallWrapper(this._daily);
1490
+ }
1491
+ setupRecorder() {
1492
+ this._mediaStreamRecorder = new (0, $5fc11d7bc0d20724$export$2934cf2d25c67a48)({
1493
+ sampleRate: $683f111f61e07358$export$b1ca982aa1e488c1.RECORDER_SAMPLE_RATE
1494
+ });
1495
+ }
1496
+ handleUserAudioStream(data) {
1497
+ this._audioQueue.push(data);
1498
+ }
1499
+ flushAudioQueue() {
1500
+ const batchSize = 10; // Number of buffers to send in one message
1501
+ if (this._audioQueue.length === 0) return;
1502
+ (0, $7zbD8$logger).debug(`Will flush audio queue: ${this._audioQueue.length}`);
1503
+ while(this._audioQueue.length > 0){
1504
+ const batch = [];
1505
+ // Collect up to `batchSize` items
1506
+ while(batch.length < batchSize && this._audioQueue.length > 0){
1507
+ const queuedData = this._audioQueue.shift();
1508
+ if (queuedData) batch.push(queuedData);
1509
+ }
1510
+ if (batch.length > 0) this._sendAudioBatch(batch);
1511
+ }
1512
+ }
1513
+ _sendAudioBatch(dataBatch) {
1514
+ const encodedBatch = dataBatch.map((data)=>{
1515
+ const pcmByteArray = new Uint8Array(data);
1516
+ return btoa(String.fromCharCode(...pcmByteArray));
1517
+ });
1518
+ const rtviMessage = {
1519
+ id: "raw-audio-batch",
1520
+ label: "rtvi-ai",
1521
+ type: "raw-audio-batch",
1522
+ data: {
1523
+ base64AudioBatch: encodedBatch,
1524
+ sampleRate: $683f111f61e07358$export$b1ca982aa1e488c1.RECORDER_SAMPLE_RATE,
1525
+ numChannels: 1
1526
+ }
1527
+ };
1528
+ this.sendMessage(rtviMessage);
1529
+ }
1530
+ initialize(options, messageHandler) {
1531
+ if (this._bufferLocalAudioUntilBotReady) this.setupRecorder();
1532
+ this._callbacks = options.callbacks ?? {};
1533
+ this._onMessage = messageHandler;
1534
+ if (this._dailyFactoryOptions.startVideoOff == null || options.enableCam != null) // Default is cam off
1535
+ this._dailyFactoryOptions.startVideoOff = !(options.enableCam ?? false);
1536
+ if (this._dailyFactoryOptions.startAudioOff == null || options.enableMic != null) // Default is mic on
1537
+ this._dailyFactoryOptions.startAudioOff = !(options.enableMic ?? true);
35
1538
  this.attachEventListeners();
36
1539
  this.state = "disconnected";
37
1540
  (0, $7zbD8$logger).debug("[RTVI Transport] Initialized");
38
1541
  }
1542
+ get dailyCallClient() {
1543
+ return this._dailyWrapper.proxy;
1544
+ }
39
1545
  get state() {
40
1546
  return this._state;
41
1547
  }
@@ -106,7 +1612,7 @@ class $683f111f61e07358$export$b1ca982aa1e488c1 extends (0, $7zbD8$Transport) {
106
1612
  return this._daily.localScreenAudio() || this._daily.localScreenVideo();
107
1613
  }
108
1614
  tracks() {
109
- const participants = this._daily?.participants() ?? {};
1615
+ const participants = this._daily.participants() ?? {};
110
1616
  const bot = participants?.[this._botId];
111
1617
  const tracks = {
112
1618
  local: {
@@ -122,6 +1628,26 @@ class $683f111f61e07358$export$b1ca982aa1e488c1 extends (0, $7zbD8$Transport) {
122
1628
  };
123
1629
  return tracks;
124
1630
  }
1631
+ async startRecording() {
1632
+ try {
1633
+ (0, $7zbD8$logger).info("[RTVI Transport] Initializing recording");
1634
+ await this._mediaStreamRecorder.record((data)=>{
1635
+ this.handleUserAudioStream(data.mono);
1636
+ }, $683f111f61e07358$export$b1ca982aa1e488c1.RECORDER_CHUNK_SIZE);
1637
+ this._onMessage({
1638
+ type: $683f111f61e07358$export$ef180de88fd317cc.AUDIO_BUFFERING_STARTED,
1639
+ data: {}
1640
+ });
1641
+ (0, $7zbD8$logger).info("[RTVI Transport] Recording Initialized");
1642
+ } catch (e) {
1643
+ const err = e;
1644
+ if (!err.message.includes("Already recording")) (0, $7zbD8$logger).error("Error starting recording", e);
1645
+ }
1646
+ }
1647
+ async preAuth(dailyFactoryOptions) {
1648
+ this._dailyFactoryOptions = dailyFactoryOptions;
1649
+ await this._daily.preAuth(dailyFactoryOptions);
1650
+ }
125
1651
  async initDevices() {
126
1652
  if (!this._daily) throw new (0, $7zbD8$RTVIError)("Transport instance not initialized");
127
1653
  this.state = "initializing";
@@ -130,14 +1656,14 @@ class $683f111f61e07358$export$b1ca982aa1e488c1 extends (0, $7zbD8$Transport) {
130
1656
  const cams = devices.filter((d)=>d.kind === "videoinput");
131
1657
  const mics = devices.filter((d)=>d.kind === "audioinput");
132
1658
  const speakers = devices.filter((d)=>d.kind === "audiooutput");
1659
+ this._selectedCam = infos.camera;
1660
+ this._selectedMic = infos.mic;
1661
+ this._selectedSpeaker = infos.speaker;
133
1662
  this._callbacks.onAvailableCamsUpdated?.(cams);
134
1663
  this._callbacks.onAvailableMicsUpdated?.(mics);
135
1664
  this._callbacks.onAvailableSpeakersUpdated?.(speakers);
136
- this._selectedCam = infos.camera;
137
1665
  this._callbacks.onCamUpdated?.(infos.camera);
138
- this._selectedMic = infos.mic;
139
1666
  this._callbacks.onMicUpdated?.(infos.mic);
140
- this._selectedSpeaker = infos.speaker;
141
1667
  this._callbacks.onSpeakerUpdated?.(infos.speaker);
142
1668
  // Instantiate audio observers
143
1669
  if (!this._daily.isLocalAudioLevelObserverRunning()) await this._daily.startLocalAudioLevelObserver(100);
@@ -148,14 +1674,15 @@ class $683f111f61e07358$export$b1ca982aa1e488c1 extends (0, $7zbD8$Transport) {
148
1674
  if (!this._daily) throw new (0, $7zbD8$RTVIError)("Transport instance not initialized");
149
1675
  if (abortController.signal.aborted) return;
150
1676
  this.state = "connecting";
1677
+ const opts = this._dailyFactoryOptions;
1678
+ opts.url = authBundle.room_url ?? opts.url;
1679
+ if (authBundle.token != null) opts.token = authBundle.token;
151
1680
  try {
152
- await this._daily.join({
153
- url: authBundle.room_url,
154
- token: authBundle.token
155
- });
1681
+ await this._daily.join(opts);
156
1682
  const room = await this._daily.room();
157
1683
  if (room && "id" in room) this._expiry = room.config?.exp;
158
1684
  } catch (e) {
1685
+ (0, $7zbD8$logger).error("Failed to join room", e);
159
1686
  this.state = "error";
160
1687
  throw new (0, $7zbD8$TransportStartError)();
161
1688
  }
@@ -166,16 +1693,30 @@ class $683f111f61e07358$export$b1ca982aa1e488c1 extends (0, $7zbD8$Transport) {
166
1693
  async sendReadyMessage() {
167
1694
  return new Promise((resolve)=>{
168
1695
  (async ()=>{
169
- this._daily.on("track-started", (ev)=>{
1696
+ const readyHandler = (ev)=>{
170
1697
  if (!ev.participant?.local) {
171
1698
  this.state = "ready";
1699
+ this.flushAudioQueue();
172
1700
  this.sendMessage((0, $7zbD8$RTVIMessage).clientReady());
1701
+ this.stopRecording();
1702
+ this._daily.off("track-started", readyHandler);
173
1703
  resolve();
174
1704
  }
175
- });
1705
+ };
1706
+ this._daily.on("track-started", readyHandler);
176
1707
  })();
177
1708
  });
178
1709
  }
1710
+ stopRecording() {
1711
+ if (this._mediaStreamRecorder && this._mediaStreamRecorder.getStatus() !== "ended") {
1712
+ // disconnecting, we don't need to record anymore
1713
+ this._mediaStreamRecorder.end();
1714
+ this._onMessage({
1715
+ type: $683f111f61e07358$export$ef180de88fd317cc.AUDIO_BUFFERING_STOPPED,
1716
+ data: {}
1717
+ });
1718
+ }
1719
+ }
179
1720
  attachEventListeners() {
180
1721
  this._daily.on("available-devices-updated", this.handleAvailableDevicesUpdated.bind(this));
181
1722
  this._daily.on("selected-devices-updated", this.handleSelectedDevicesUpdated.bind(this));
@@ -190,10 +1731,13 @@ class $683f111f61e07358$export$b1ca982aa1e488c1 extends (0, $7zbD8$Transport) {
190
1731
  this._daily.on("nonfatal-error", this.handleNonFatalError.bind(this));
191
1732
  }
192
1733
  async disconnect() {
1734
+ this.state = "disconnecting";
193
1735
  this._daily.stopLocalAudioLevelObserver();
194
1736
  this._daily.stopRemoteParticipantsAudioLevelObserver();
1737
+ this._audioQueue = [];
1738
+ this._currentAudioTrack = null;
1739
+ this.stopRecording();
195
1740
  await this._daily.leave();
196
- await this._daily.destroy();
197
1741
  }
198
1742
  sendMessage(message) {
199
1743
  this._daily.sendAppMessage(message, "*");
@@ -225,9 +1769,34 @@ class $683f111f61e07358$export$b1ca982aa1e488c1 extends (0, $7zbD8$Transport) {
225
1769
  this._callbacks.onSpeakerUpdated?.(ev.devices.speaker);
226
1770
  }
227
1771
  }
1772
+ async handleLocalAudioTrack(track) {
1773
+ if (this.state == "ready" || !this._bufferLocalAudioUntilBotReady) return;
1774
+ const status = this._mediaStreamRecorder.getStatus();
1775
+ switch(status){
1776
+ case "ended":
1777
+ await this._mediaStreamRecorder.begin(track);
1778
+ await this.startRecording();
1779
+ break;
1780
+ case "paused":
1781
+ await this.startRecording();
1782
+ break;
1783
+ case "recording":
1784
+ default:
1785
+ if (this._currentAudioTrack !== track) {
1786
+ await this._mediaStreamRecorder.end();
1787
+ await this._mediaStreamRecorder.begin(track);
1788
+ await this.startRecording();
1789
+ } else (0, $7zbD8$logger).warn("track-started event received for current track and already recording");
1790
+ break;
1791
+ }
1792
+ this._currentAudioTrack = track;
1793
+ }
228
1794
  handleTrackStarted(ev) {
229
1795
  if (ev.type === "screenAudio" || ev.type === "screenVideo") this._callbacks.onScreenTrackStarted?.(ev.track, ev.participant ? $683f111f61e07358$var$dailyParticipantToParticipant(ev.participant) : undefined);
230
- else this._callbacks.onTrackStarted?.(ev.track, ev.participant ? $683f111f61e07358$var$dailyParticipantToParticipant(ev.participant) : undefined);
1796
+ else {
1797
+ if (ev.participant?.local && ev.track.kind === "audio") this.handleLocalAudioTrack(ev.track);
1798
+ this._callbacks.onTrackStarted?.(ev.track, ev.participant ? $683f111f61e07358$var$dailyParticipantToParticipant(ev.participant) : undefined);
1799
+ }
231
1800
  }
232
1801
  handleTrackStopped(ev) {
233
1802
  if (ev.type === "screenAudio" || ev.type === "screenVideo") this._callbacks.onScreenTrackStopped?.(ev.track, ev.participant ? $683f111f61e07358$var$dailyParticipantToParticipant(ev.participant) : undefined);
@@ -260,7 +1829,7 @@ class $683f111f61e07358$export$b1ca982aa1e488c1 extends (0, $7zbD8$Transport) {
260
1829
  }
261
1830
  }
262
1831
  handleLeftMeeting() {
263
- this.state = "disconnecting";
1832
+ this.state = "disconnected";
264
1833
  this._botId = "";
265
1834
  this._callbacks.onDisconnected?.();
266
1835
  }
@@ -272,6 +1841,8 @@ class $683f111f61e07358$export$b1ca982aa1e488c1 extends (0, $7zbD8$Transport) {
272
1841
  }
273
1842
  }
274
1843
  }
1844
+ $683f111f61e07358$export$b1ca982aa1e488c1.RECORDER_SAMPLE_RATE = 16000;
1845
+ $683f111f61e07358$export$b1ca982aa1e488c1.RECORDER_CHUNK_SIZE = 512;
275
1846
  const $683f111f61e07358$var$dailyParticipantToParticipant = (p)=>({
276
1847
  id: p.user_id,
277
1848
  local: p.local,
@@ -281,5 +1852,5 @@ const $683f111f61e07358$var$dailyParticipantToParticipant = (p)=>({
281
1852
 
282
1853
 
283
1854
 
284
- export {$683f111f61e07358$export$b1ca982aa1e488c1 as DailyTransport};
1855
+ export {$683f111f61e07358$export$ef180de88fd317cc as DailyRTVIMessageType, $683f111f61e07358$export$b1ca982aa1e488c1 as DailyTransport};
285
1856
  //# sourceMappingURL=index.module.js.map