@livekit/agents 1.0.37 → 1.0.38
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/cli.cjs.map +1 -1
- package/dist/inference/api_protos.cjs +68 -0
- package/dist/inference/api_protos.cjs.map +1 -1
- package/dist/inference/api_protos.d.cts +345 -4
- package/dist/inference/api_protos.d.ts +345 -4
- package/dist/inference/api_protos.d.ts.map +1 -1
- package/dist/inference/api_protos.js +60 -0
- package/dist/inference/api_protos.js.map +1 -1
- package/dist/inference/stt.cjs +32 -21
- package/dist/inference/stt.cjs.map +1 -1
- package/dist/inference/stt.d.ts.map +1 -1
- package/dist/inference/stt.js +34 -21
- package/dist/inference/stt.js.map +1 -1
- package/dist/ipc/inference_proc_executor.cjs.map +1 -1
- package/dist/ipc/job_proc_executor.cjs.map +1 -1
- package/dist/stt/stt.cjs +10 -0
- package/dist/stt/stt.cjs.map +1 -1
- package/dist/stt/stt.d.cts +12 -0
- package/dist/stt/stt.d.ts +12 -0
- package/dist/stt/stt.d.ts.map +1 -1
- package/dist/stt/stt.js +10 -0
- package/dist/stt/stt.js.map +1 -1
- package/dist/telemetry/traces.cjs +4 -3
- package/dist/telemetry/traces.cjs.map +1 -1
- package/dist/telemetry/traces.d.cts +2 -0
- package/dist/telemetry/traces.d.ts +2 -0
- package/dist/telemetry/traces.d.ts.map +1 -1
- package/dist/telemetry/traces.js +4 -3
- package/dist/telemetry/traces.js.map +1 -1
- package/dist/utils.cjs +6 -0
- package/dist/utils.cjs.map +1 -1
- package/dist/utils.d.cts +2 -0
- package/dist/utils.d.ts +2 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +6 -0
- package/dist/utils.js.map +1 -1
- package/dist/voice/agent.cjs +5 -0
- package/dist/voice/agent.cjs.map +1 -1
- package/dist/voice/agent.d.ts.map +1 -1
- package/dist/voice/agent.js +5 -0
- package/dist/voice/agent.js.map +1 -1
- package/dist/voice/agent_activity.cjs +49 -23
- package/dist/voice/agent_activity.cjs.map +1 -1
- package/dist/voice/agent_activity.d.cts +1 -1
- package/dist/voice/agent_activity.d.ts +1 -1
- package/dist/voice/agent_activity.d.ts.map +1 -1
- package/dist/voice/agent_activity.js +50 -24
- package/dist/voice/agent_activity.js.map +1 -1
- package/dist/voice/agent_session.cjs +7 -5
- package/dist/voice/agent_session.cjs.map +1 -1
- package/dist/voice/agent_session.d.cts +5 -2
- package/dist/voice/agent_session.d.ts +5 -2
- package/dist/voice/agent_session.d.ts.map +1 -1
- package/dist/voice/agent_session.js +7 -5
- package/dist/voice/agent_session.js.map +1 -1
- package/dist/voice/audio_recognition.cjs +3 -1
- package/dist/voice/audio_recognition.cjs.map +1 -1
- package/dist/voice/audio_recognition.d.ts.map +1 -1
- package/dist/voice/audio_recognition.js +3 -1
- package/dist/voice/audio_recognition.js.map +1 -1
- package/dist/voice/avatar/datastream_io.cjs +6 -0
- package/dist/voice/avatar/datastream_io.cjs.map +1 -1
- package/dist/voice/avatar/datastream_io.d.cts +1 -0
- package/dist/voice/avatar/datastream_io.d.ts +1 -0
- package/dist/voice/avatar/datastream_io.d.ts.map +1 -1
- package/dist/voice/avatar/datastream_io.js +6 -0
- package/dist/voice/avatar/datastream_io.js.map +1 -1
- package/dist/voice/background_audio.cjs.map +1 -1
- package/dist/voice/generation.cjs +14 -5
- package/dist/voice/generation.cjs.map +1 -1
- package/dist/voice/generation.d.cts +3 -2
- package/dist/voice/generation.d.ts +3 -2
- package/dist/voice/generation.d.ts.map +1 -1
- package/dist/voice/generation.js +14 -5
- package/dist/voice/generation.js.map +1 -1
- package/dist/voice/io.cjs +12 -0
- package/dist/voice/io.cjs.map +1 -1
- package/dist/voice/io.d.cts +19 -1
- package/dist/voice/io.d.ts +19 -1
- package/dist/voice/io.d.ts.map +1 -1
- package/dist/voice/io.js +12 -0
- package/dist/voice/io.js.map +1 -1
- package/dist/voice/recorder_io/recorder_io.cjs +91 -28
- package/dist/voice/recorder_io/recorder_io.cjs.map +1 -1
- package/dist/voice/recorder_io/recorder_io.d.cts +7 -1
- package/dist/voice/recorder_io/recorder_io.d.ts +7 -1
- package/dist/voice/recorder_io/recorder_io.d.ts.map +1 -1
- package/dist/voice/recorder_io/recorder_io.js +91 -28
- package/dist/voice/recorder_io/recorder_io.js.map +1 -1
- package/dist/voice/room_io/_input.cjs +40 -11
- package/dist/voice/room_io/_input.cjs.map +1 -1
- package/dist/voice/room_io/_input.d.cts +4 -1
- package/dist/voice/room_io/_input.d.ts +4 -1
- package/dist/voice/room_io/_input.d.ts.map +1 -1
- package/dist/voice/room_io/_input.js +31 -2
- package/dist/voice/room_io/_input.js.map +1 -1
- package/dist/voice/room_io/_output.cjs +6 -0
- package/dist/voice/room_io/_output.cjs.map +1 -1
- package/dist/voice/room_io/_output.d.cts +1 -0
- package/dist/voice/room_io/_output.d.ts +1 -0
- package/dist/voice/room_io/_output.d.ts.map +1 -1
- package/dist/voice/room_io/_output.js +6 -0
- package/dist/voice/room_io/_output.js.map +1 -1
- package/dist/voice/room_io/room_io.cjs.map +1 -1
- package/dist/voice/room_io/room_io.d.cts +2 -2
- package/dist/voice/room_io/room_io.d.ts +2 -2
- package/dist/voice/room_io/room_io.d.ts.map +1 -1
- package/dist/voice/room_io/room_io.js.map +1 -1
- package/dist/voice/speech_handle.cjs +2 -0
- package/dist/voice/speech_handle.cjs.map +1 -1
- package/dist/voice/speech_handle.d.cts +3 -0
- package/dist/voice/speech_handle.d.ts +3 -0
- package/dist/voice/speech_handle.d.ts.map +1 -1
- package/dist/voice/speech_handle.js +2 -0
- package/dist/voice/speech_handle.js.map +1 -1
- package/package.json +1 -1
- package/src/inference/api_protos.ts +83 -0
- package/src/inference/stt.ts +39 -22
- package/src/stt/stt.ts +21 -0
- package/src/telemetry/traces.ts +6 -2
- package/src/utils.ts +7 -0
- package/src/voice/agent.ts +9 -0
- package/src/voice/agent_activity.ts +72 -26
- package/src/voice/agent_session.ts +6 -5
- package/src/voice/audio_recognition.ts +2 -0
- package/src/voice/avatar/datastream_io.ts +8 -0
- package/src/voice/generation.ts +24 -12
- package/src/voice/io.ts +27 -5
- package/src/voice/recorder_io/recorder_io.ts +123 -31
- package/src/voice/room_io/_input.ts +32 -4
- package/src/voice/room_io/_output.ts +8 -0
- package/src/voice/room_io/room_io.ts +3 -1
- package/src/voice/speech_handle.ts +4 -0
|
@@ -82,7 +82,8 @@ class RecorderIO {
|
|
|
82
82
|
return this.outRecord;
|
|
83
83
|
}
|
|
84
84
|
writeCb(buf) {
|
|
85
|
-
|
|
85
|
+
var _a;
|
|
86
|
+
const inputBuf = this.inRecord.takeBuf((_a = this.outRecord) == null ? void 0 : _a._lastSpeechEndTime);
|
|
86
87
|
this.inChan.write(inputBuf);
|
|
87
88
|
this.outChan.write(buf);
|
|
88
89
|
}
|
|
@@ -93,7 +94,16 @@ class RecorderIO {
|
|
|
93
94
|
return this._outputPath;
|
|
94
95
|
}
|
|
95
96
|
get recordingStartedAt() {
|
|
96
|
-
|
|
97
|
+
var _a, _b;
|
|
98
|
+
const inT = (_a = this.inRecord) == null ? void 0 : _a.startedWallTime;
|
|
99
|
+
const outT = (_b = this.outRecord) == null ? void 0 : _b.startedWallTime;
|
|
100
|
+
if (inT === void 0) {
|
|
101
|
+
return outT;
|
|
102
|
+
}
|
|
103
|
+
if (outT === void 0) {
|
|
104
|
+
return inT;
|
|
105
|
+
}
|
|
106
|
+
return Math.min(inT, outT);
|
|
97
107
|
}
|
|
98
108
|
/**
|
|
99
109
|
* Forward task: periodically flush input buffer to encoder
|
|
@@ -108,7 +118,7 @@ class RecorderIO {
|
|
|
108
118
|
if (this.outRecord.hasPendingData) {
|
|
109
119
|
continue;
|
|
110
120
|
}
|
|
111
|
-
const inputBuf = this.inRecord.takeBuf();
|
|
121
|
+
const inputBuf = this.inRecord.takeBuf(this.outRecord._lastSpeechEndTime);
|
|
112
122
|
this.inChan.write(inputBuf).catch((err) => this.logger.error({ err }, "Error writing RecorderIO input buffer"));
|
|
113
123
|
this.outChan.write([]).catch((err) => this.logger.error({ err }, "Error writing RecorderIO output buffer"));
|
|
114
124
|
}
|
|
@@ -255,6 +265,8 @@ class RecorderAudioInput extends AudioInput {
|
|
|
255
265
|
recorderIO;
|
|
256
266
|
accFrames = [];
|
|
257
267
|
_startedWallTime;
|
|
268
|
+
_padded = false;
|
|
269
|
+
logger = log();
|
|
258
270
|
constructor(recorderIO, source) {
|
|
259
271
|
super();
|
|
260
272
|
this.recorderIO = recorderIO;
|
|
@@ -269,10 +281,31 @@ class RecorderAudioInput extends AudioInput {
|
|
|
269
281
|
}
|
|
270
282
|
/**
|
|
271
283
|
* Take accumulated frames and clear the buffer
|
|
284
|
+
* @param padSince - If provided and input started after this time, pad with silence
|
|
272
285
|
*/
|
|
273
|
-
takeBuf() {
|
|
274
|
-
|
|
286
|
+
takeBuf(padSince) {
|
|
287
|
+
let frames = this.accFrames;
|
|
275
288
|
this.accFrames = [];
|
|
289
|
+
if (padSince !== void 0 && this._startedWallTime !== void 0 && this._startedWallTime > padSince && !this._padded && frames.length > 0) {
|
|
290
|
+
const padding = this._startedWallTime - padSince;
|
|
291
|
+
this.logger.warn(
|
|
292
|
+
{
|
|
293
|
+
lastAgentSpeechTime: padSince,
|
|
294
|
+
inputStartedTime: this._startedWallTime
|
|
295
|
+
},
|
|
296
|
+
"input speech started after last agent speech ended"
|
|
297
|
+
);
|
|
298
|
+
this._padded = true;
|
|
299
|
+
const firstFrame = frames[0];
|
|
300
|
+
frames = [
|
|
301
|
+
createSilenceFrame(padding / 1e3, firstFrame.sampleRate, firstFrame.channels),
|
|
302
|
+
...frames
|
|
303
|
+
];
|
|
304
|
+
} else if (padSince !== void 0 && this._startedWallTime === void 0 && !this._padded && frames.length === 0) {
|
|
305
|
+
this.logger.warn(
|
|
306
|
+
"input speech hasn't started yet, skipping silence padding, recording may be inaccurate until the speech starts"
|
|
307
|
+
);
|
|
308
|
+
}
|
|
276
309
|
return frames;
|
|
277
310
|
}
|
|
278
311
|
/**
|
|
@@ -332,6 +365,9 @@ class RecorderAudioOutput extends AudioOutput {
|
|
|
332
365
|
writeFn;
|
|
333
366
|
accFrames = [];
|
|
334
367
|
_startedWallTime;
|
|
368
|
+
_logger = log();
|
|
369
|
+
_lastSpeechEndTime;
|
|
370
|
+
_lastSpeechStartTime;
|
|
335
371
|
// Pause tracking
|
|
336
372
|
currentPauseStart;
|
|
337
373
|
pauseWallTimes = [];
|
|
@@ -372,8 +408,25 @@ class RecorderAudioOutput extends AudioOutput {
|
|
|
372
408
|
this.pauseWallTimes = [];
|
|
373
409
|
}
|
|
374
410
|
onPlaybackFinished(options) {
|
|
375
|
-
const finishTime = Date.now();
|
|
376
|
-
|
|
411
|
+
const finishTime = this.currentPauseStart ?? Date.now();
|
|
412
|
+
const trailingSilenceDuration = Math.max(0, Date.now() - finishTime);
|
|
413
|
+
let playbackPosition = options.playbackPosition * 1e3;
|
|
414
|
+
if (this._lastSpeechStartTime === void 0) {
|
|
415
|
+
this._logger.warn(
|
|
416
|
+
{
|
|
417
|
+
finishTime,
|
|
418
|
+
playbackPosition,
|
|
419
|
+
interrupted: options.interrupted
|
|
420
|
+
},
|
|
421
|
+
"playback finished before speech started"
|
|
422
|
+
);
|
|
423
|
+
playbackPosition = 0;
|
|
424
|
+
}
|
|
425
|
+
playbackPosition = Math.max(
|
|
426
|
+
0,
|
|
427
|
+
Math.min(finishTime - (this._lastSpeechStartTime ?? 0), playbackPosition)
|
|
428
|
+
);
|
|
429
|
+
super.onPlaybackFinished({ ...options, playbackPosition: playbackPosition / 1e3 });
|
|
377
430
|
if (!this.recorderIO.recording) {
|
|
378
431
|
return;
|
|
379
432
|
}
|
|
@@ -383,23 +436,25 @@ class RecorderAudioOutput extends AudioOutput {
|
|
|
383
436
|
}
|
|
384
437
|
if (this.accFrames.length === 0) {
|
|
385
438
|
this.resetPauseState();
|
|
439
|
+
this._lastSpeechEndTime = Date.now();
|
|
440
|
+
this._lastSpeechStartTime = void 0;
|
|
386
441
|
return;
|
|
387
442
|
}
|
|
388
|
-
const playbackPosition = options.playbackPosition;
|
|
389
443
|
const pauseEvents = [];
|
|
444
|
+
let playbackStartTime = finishTime - playbackPosition;
|
|
390
445
|
if (this.pauseWallTimes.length > 0) {
|
|
391
446
|
const totalPauseDuration = this.pauseWallTimes.reduce(
|
|
392
447
|
(sum, [start, end]) => sum + (end - start),
|
|
393
448
|
0
|
|
394
449
|
);
|
|
395
|
-
|
|
450
|
+
playbackStartTime = finishTime - playbackPosition - totalPauseDuration;
|
|
396
451
|
let accumulatedPause = 0;
|
|
397
452
|
for (const [pauseStart, pauseEnd] of this.pauseWallTimes) {
|
|
398
|
-
let position =
|
|
399
|
-
const duration =
|
|
453
|
+
let position = pauseStart - playbackStartTime - accumulatedPause;
|
|
454
|
+
const duration = pauseEnd - pauseStart;
|
|
400
455
|
position = Math.max(0, Math.min(position, playbackPosition));
|
|
401
456
|
pauseEvents.push([position, duration]);
|
|
402
|
-
accumulatedPause +=
|
|
457
|
+
accumulatedPause += duration;
|
|
403
458
|
}
|
|
404
459
|
}
|
|
405
460
|
const buf = [];
|
|
@@ -410,29 +465,29 @@ class RecorderAudioOutput extends AudioOutput {
|
|
|
410
465
|
let shouldBreak = false;
|
|
411
466
|
for (const frame of this.accFrames) {
|
|
412
467
|
let currentFrame = frame;
|
|
413
|
-
const frameDuration = frame.samplesPerChannel / frame.sampleRate;
|
|
468
|
+
const frameDuration = frame.samplesPerChannel / frame.sampleRate * 1e3;
|
|
414
469
|
if (frameDuration + accDur > playbackPosition) {
|
|
415
|
-
const [left] = splitFrame(currentFrame, playbackPosition - accDur);
|
|
470
|
+
const [left] = splitFrame(currentFrame, (playbackPosition - accDur) / 1e3);
|
|
416
471
|
currentFrame = left;
|
|
417
472
|
shouldBreak = true;
|
|
418
473
|
}
|
|
419
474
|
while (pauseIdx < pauseEvents.length && pauseEvents[pauseIdx][0] <= accDur) {
|
|
420
475
|
const [, pauseDur] = pauseEvents[pauseIdx];
|
|
421
|
-
buf.push(createSilenceFrame(pauseDur, sampleRate, numChannels));
|
|
476
|
+
buf.push(createSilenceFrame(pauseDur / 1e3, sampleRate, numChannels));
|
|
422
477
|
pauseIdx++;
|
|
423
478
|
}
|
|
424
|
-
const currentFrameDuration = currentFrame.samplesPerChannel / currentFrame.sampleRate;
|
|
479
|
+
const currentFrameDuration = currentFrame.samplesPerChannel / currentFrame.sampleRate * 1e3;
|
|
425
480
|
while (pauseIdx < pauseEvents.length && pauseEvents[pauseIdx][0] < accDur + currentFrameDuration) {
|
|
426
481
|
const [pausePos, pauseDur] = pauseEvents[pauseIdx];
|
|
427
|
-
const [left, right] = splitFrame(currentFrame, pausePos - accDur);
|
|
482
|
+
const [left, right] = splitFrame(currentFrame, (pausePos - accDur) / 1e3);
|
|
428
483
|
buf.push(left);
|
|
429
|
-
accDur += left.samplesPerChannel / left.sampleRate;
|
|
430
|
-
buf.push(createSilenceFrame(pauseDur, sampleRate, numChannels));
|
|
484
|
+
accDur += left.samplesPerChannel / left.sampleRate * 1e3;
|
|
485
|
+
buf.push(createSilenceFrame(pauseDur / 1e3, sampleRate, numChannels));
|
|
431
486
|
currentFrame = right;
|
|
432
487
|
pauseIdx++;
|
|
433
488
|
}
|
|
434
489
|
buf.push(currentFrame);
|
|
435
|
-
accDur += currentFrame.samplesPerChannel / currentFrame.sampleRate;
|
|
490
|
+
accDur += currentFrame.samplesPerChannel / currentFrame.sampleRate * 1e3;
|
|
436
491
|
if (shouldBreak) {
|
|
437
492
|
break;
|
|
438
493
|
}
|
|
@@ -440,26 +495,34 @@ class RecorderAudioOutput extends AudioOutput {
|
|
|
440
495
|
while (pauseIdx < pauseEvents.length) {
|
|
441
496
|
const [pausePos, pauseDur] = pauseEvents[pauseIdx];
|
|
442
497
|
if (pausePos <= playbackPosition) {
|
|
443
|
-
buf.push(createSilenceFrame(pauseDur, sampleRate, numChannels));
|
|
498
|
+
buf.push(createSilenceFrame(pauseDur / 1e3, sampleRate, numChannels));
|
|
444
499
|
}
|
|
445
500
|
pauseIdx++;
|
|
446
501
|
}
|
|
447
502
|
if (buf.length > 0) {
|
|
503
|
+
if (trailingSilenceDuration > 0) {
|
|
504
|
+
buf.push(createSilenceFrame(trailingSilenceDuration / 1e3, sampleRate, numChannels));
|
|
505
|
+
}
|
|
448
506
|
this.writeFn(buf);
|
|
449
507
|
}
|
|
450
508
|
this.accFrames = [];
|
|
451
509
|
this.resetPauseState();
|
|
510
|
+
this._lastSpeechEndTime = Date.now();
|
|
511
|
+
this._lastSpeechStartTime = void 0;
|
|
452
512
|
}
|
|
453
513
|
async captureFrame(frame) {
|
|
514
|
+
if (this.nextInChain) {
|
|
515
|
+
await this.nextInChain.captureFrame(frame);
|
|
516
|
+
}
|
|
454
517
|
await super.captureFrame(frame);
|
|
455
518
|
if (this.recorderIO.recording) {
|
|
456
|
-
if (this._startedWallTime === void 0) {
|
|
457
|
-
this._startedWallTime = Date.now();
|
|
458
|
-
}
|
|
459
519
|
this.accFrames.push(frame);
|
|
460
520
|
}
|
|
461
|
-
if (this.
|
|
462
|
-
|
|
521
|
+
if (this._startedWallTime === void 0) {
|
|
522
|
+
this._startedWallTime = Date.now();
|
|
523
|
+
}
|
|
524
|
+
if (this._lastSpeechStartTime === void 0) {
|
|
525
|
+
this._lastSpeechStartTime = Date.now();
|
|
463
526
|
}
|
|
464
527
|
}
|
|
465
528
|
flush() {
|
|
@@ -474,8 +537,8 @@ class RecorderAudioOutput extends AudioOutput {
|
|
|
474
537
|
}
|
|
475
538
|
}
|
|
476
539
|
}
|
|
477
|
-
function createSilenceFrame(
|
|
478
|
-
const samples = Math.floor(
|
|
540
|
+
function createSilenceFrame(durationInS, sampleRate, numChannels) {
|
|
541
|
+
const samples = Math.floor(durationInS * sampleRate);
|
|
479
542
|
const data = new Int16Array(samples * numChannels);
|
|
480
543
|
return new AudioFrame(data, sampleRate, numChannels, samples);
|
|
481
544
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/voice/recorder_io/recorder_io.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport ffmpegInstaller from '@ffmpeg-installer/ffmpeg';\nimport { Mutex } from '@livekit/mutex';\nimport { AudioFrame, AudioResampler } from '@livekit/rtc-node';\nimport ffmpeg from 'fluent-ffmpeg';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { PassThrough } from 'node:stream';\nimport type { ReadableStream } from 'node:stream/web';\nimport { TransformStream } from 'node:stream/web';\nimport { log } from '../../log.js';\nimport { isStreamReaderReleaseError } from '../../stream/deferred_stream.js';\nimport { type StreamChannel, createStreamChannel } from '../../stream/stream_channel.js';\nimport { Future, Task, cancelAndWait, delay } from '../../utils.js';\nimport type { AgentSession } from '../agent_session.js';\nimport { AudioInput, AudioOutput, type PlaybackFinishedEvent } from '../io.js';\n\nffmpeg.setFfmpegPath(ffmpegInstaller.path);\n\nconst WRITE_INTERVAL_MS = 2500;\nconst DEFAULT_SAMPLE_RATE = 48000;\n\nexport interface RecorderOptions {\n agentSession: AgentSession;\n sampleRate?: number;\n}\n\ninterface ResampleAndMixOptions {\n frames: AudioFrame[];\n resampler: AudioResampler | undefined;\n flush?: boolean;\n}\n\nexport class RecorderIO {\n private inRecord?: RecorderAudioInput;\n private outRecord?: RecorderAudioOutput;\n\n private inChan: StreamChannel<AudioFrame[]> = createStreamChannel<AudioFrame[]>();\n private outChan: StreamChannel<AudioFrame[]> = createStreamChannel<AudioFrame[]>();\n\n private session: AgentSession;\n private sampleRate: number;\n\n private _outputPath?: string;\n private forwardTask?: Task<void>;\n private encodeTask?: Task<void>;\n\n private closeFuture: Future<void> = new Future();\n private lock: Mutex = new Mutex();\n private started: boolean = false;\n\n // FFmpeg streaming state\n private pcmStream?: PassThrough;\n private ffmpegPromise?: Promise<void>;\n private inResampler?: AudioResampler;\n private outResampler?: AudioResampler;\n\n private logger = log();\n\n constructor(opts: RecorderOptions) {\n const { agentSession, sampleRate = DEFAULT_SAMPLE_RATE } = opts;\n\n this.session = agentSession;\n this.sampleRate = sampleRate;\n }\n\n async start(outputPath: string): Promise<void> {\n const unlock = await this.lock.lock();\n\n try {\n if (this.started) return;\n\n if (!this.inRecord || !this.outRecord) {\n throw new Error(\n 'RecorderIO not properly initialized: both `recordInput()` and `recordOutput()` must be called before starting the recorder.',\n );\n }\n\n this._outputPath = outputPath;\n this.started = true;\n this.closeFuture = new Future();\n\n // Ensure output directory exists\n const dir = path.dirname(outputPath);\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true });\n }\n\n this.forwardTask = Task.from(({ signal }) => this.forward(signal));\n this.encodeTask = Task.from(() => this.encode(), undefined, 'recorder_io_encode_task');\n } finally {\n unlock();\n }\n }\n\n async close(): Promise<void> {\n const unlock = await this.lock.lock();\n\n try {\n if (!this.started) return;\n\n await this.inChan.close();\n await this.outChan.close();\n await this.closeFuture.await;\n await cancelAndWait([this.forwardTask!, this.encodeTask!]);\n\n this.started = false;\n } finally {\n unlock();\n }\n }\n\n recordInput(audioInput: AudioInput): RecorderAudioInput {\n this.inRecord = new RecorderAudioInput(this, audioInput);\n return this.inRecord;\n }\n\n recordOutput(audioOutput: AudioOutput): RecorderAudioOutput {\n this.outRecord = new RecorderAudioOutput(this, audioOutput, (buf) => this.writeCb(buf));\n return this.outRecord;\n }\n\n private writeCb(buf: AudioFrame[]): void {\n const inputBuf = this.inRecord!.takeBuf();\n this.inChan.write(inputBuf);\n this.outChan.write(buf);\n }\n\n get recording(): boolean {\n return this.started;\n }\n\n get outputPath(): string | undefined {\n return this._outputPath;\n }\n\n get recordingStartedAt(): number | undefined {\n // Use session start time to align with trace timestamps\n return this.session._startedAt;\n }\n\n /**\n * Forward task: periodically flush input buffer to encoder\n */\n private async forward(signal: AbortSignal): Promise<void> {\n while (!signal.aborted) {\n try {\n await delay(WRITE_INTERVAL_MS, { signal });\n } catch {\n // Aborted\n break;\n }\n\n if (this.outRecord!.hasPendingData) {\n // If the output is currently playing audio, wait for it to stay in sync\n continue;\n }\n\n // Flush input buffer\n const inputBuf = this.inRecord!.takeBuf();\n this.inChan\n .write(inputBuf)\n .catch((err) => this.logger.error({ err }, 'Error writing RecorderIO input buffer'));\n this.outChan\n .write([])\n .catch((err) => this.logger.error({ err }, 'Error writing RecorderIO output buffer'));\n }\n }\n\n /**\n * Start FFmpeg process for streaming encoding\n */\n private startFFmpeg(): void {\n if (this.pcmStream) return;\n\n this.pcmStream = new PassThrough();\n\n this.ffmpegPromise = new Promise<void>((resolve, reject) => {\n ffmpeg(this.pcmStream!)\n .inputFormat('s16le')\n .inputOptions([`-ar ${this.sampleRate}`, '-ac 2'])\n .audioCodec('libopus')\n .audioChannels(2)\n .audioFrequency(this.sampleRate)\n .format('ogg')\n .output(this._outputPath!)\n .on('end', () => {\n this.logger.debug('FFmpeg encoding finished');\n resolve();\n })\n .on('error', (err) => {\n // Ignore errors from intentional stream closure or SIGINT during shutdown\n if (\n err.message?.includes('Output stream closed') ||\n err.message?.includes('received signal 2') ||\n err.message?.includes('SIGKILL') ||\n err.message?.includes('SIGINT')\n ) {\n resolve();\n } else {\n this.logger.error({ err }, 'FFmpeg encoding error');\n reject(err);\n }\n })\n .run();\n });\n }\n\n /**\n * Resample and mix frames to mono Float32\n */\n private resampleAndMix(opts: ResampleAndMixOptions): {\n samples: Float32Array;\n resampler: AudioResampler | undefined;\n } {\n const INV_INT16 = 1.0 / 32768.0;\n const { frames, flush = false } = opts;\n let { resampler } = opts;\n\n if (frames.length === 0 && !flush) {\n return { samples: new Float32Array(0), resampler };\n }\n\n if (!resampler && frames.length > 0) {\n const firstFrame = frames[0]!;\n resampler = new AudioResampler(firstFrame.sampleRate, this.sampleRate, firstFrame.channels);\n }\n\n const resampledFrames: AudioFrame[] = [];\n for (const frame of frames) {\n if (resampler) {\n resampledFrames.push(...resampler.push(frame));\n }\n }\n\n if (flush && resampler) {\n resampledFrames.push(...resampler.flush());\n }\n\n const totalSamples = resampledFrames.reduce((acc, frame) => acc + frame.samplesPerChannel, 0);\n const samples = new Float32Array(totalSamples);\n\n let pos = 0;\n for (const frame of resampledFrames) {\n const data = frame.data;\n const numChannels = frame.channels;\n for (let i = 0; i < frame.samplesPerChannel; i++) {\n let sum = 0;\n for (let ch = 0; ch < numChannels; ch++) {\n sum += data[i * numChannels + ch]!;\n }\n samples[pos++] = (sum / numChannels) * INV_INT16;\n }\n }\n\n return { samples, resampler };\n }\n\n /**\n * Write PCM chunk to FFmpeg stream\n */\n private writePCM(leftSamples: Float32Array, rightSamples: Float32Array): void {\n if (!this.pcmStream) {\n this.startFFmpeg();\n }\n\n // Handle length mismatch by prepending silence\n if (leftSamples.length !== rightSamples.length) {\n const diff = Math.abs(leftSamples.length - rightSamples.length);\n if (leftSamples.length < rightSamples.length) {\n this.logger.warn(\n `Input is shorter by ${diff} samples; silence has been prepended to align the input channel.`,\n );\n const padded = new Float32Array(rightSamples.length);\n padded.set(leftSamples, diff);\n leftSamples = padded;\n } else {\n const padded = new Float32Array(leftSamples.length);\n padded.set(rightSamples, diff);\n rightSamples = padded;\n }\n }\n\n const maxLen = Math.max(leftSamples.length, rightSamples.length);\n if (maxLen <= 0) return;\n\n // Interleave stereo samples and convert back to Int16\n const stereoData = new Int16Array(maxLen * 2);\n for (let i = 0; i < maxLen; i++) {\n stereoData[i * 2] = Math.max(\n -32768,\n Math.min(32767, Math.round((leftSamples[i] ?? 0) * 32768)),\n );\n stereoData[i * 2 + 1] = Math.max(\n -32768,\n Math.min(32767, Math.round((rightSamples[i] ?? 0) * 32768)),\n );\n }\n\n this.pcmStream!.write(Buffer.from(stereoData.buffer));\n }\n\n /**\n * Encode task: read from channels, mix to stereo, stream to FFmpeg\n */\n private async encode(): Promise<void> {\n if (!this._outputPath) return;\n\n const inReader = this.inChan.stream().getReader();\n const outReader = this.outChan.stream().getReader();\n\n try {\n while (true) {\n const [inResult, outResult] = await Promise.all([inReader.read(), outReader.read()]);\n\n if (inResult.done || outResult.done) {\n break;\n }\n\n const inputBuf = inResult.value;\n const outputBuf = outResult.value;\n\n const inMixed = this.resampleAndMix({ frames: inputBuf, resampler: this.inResampler });\n this.inResampler = inMixed.resampler;\n\n const outMixed = this.resampleAndMix({\n frames: outputBuf,\n resampler: this.outResampler,\n flush: outputBuf.length > 0,\n });\n this.outResampler = outMixed.resampler;\n\n // Stream PCM data directly to FFmpeg\n this.writePCM(inMixed.samples, outMixed.samples);\n }\n\n // Close FFmpeg stream and wait for encoding to complete\n if (this.pcmStream) {\n this.pcmStream.end();\n await this.ffmpegPromise;\n }\n } catch (err) {\n this.logger.error({ err }, 'Error in encode task');\n } finally {\n inReader.releaseLock();\n outReader.releaseLock();\n\n if (!this.closeFuture.done) {\n this.closeFuture.resolve();\n }\n }\n }\n}\n\nclass RecorderAudioInput extends AudioInput {\n private source: AudioInput;\n private recorderIO: RecorderIO;\n private accFrames: AudioFrame[] = [];\n private _startedWallTime?: number;\n\n constructor(recorderIO: RecorderIO, source: AudioInput) {\n super();\n this.recorderIO = recorderIO;\n this.source = source;\n\n // Set up the intercepting stream\n this.deferredStream.setSource(this.createInterceptingStream());\n }\n\n /**\n * Wall-clock time when the first frame was captured\n */\n get startedWallTime(): number | undefined {\n return this._startedWallTime;\n }\n\n /**\n * Take accumulated frames and clear the buffer\n */\n takeBuf(): AudioFrame[] {\n const frames = this.accFrames;\n this.accFrames = [];\n return frames;\n }\n\n /**\n * Creates a stream that intercepts frames from the source,\n * accumulates them when recording, and passes them through unchanged.\n */\n private createInterceptingStream(): ReadableStream<AudioFrame> {\n const sourceStream = this.source.stream;\n const reader = sourceStream.getReader();\n\n const transform = new TransformStream<AudioFrame, AudioFrame>({\n transform: (frame, controller) => {\n // Accumulate frames when recording is active\n if (this.recorderIO.recording) {\n if (this._startedWallTime === undefined) {\n this._startedWallTime = Date.now();\n }\n this.accFrames.push(frame);\n }\n\n controller.enqueue(frame);\n },\n });\n\n const pump = async () => {\n const writer = transform.writable.getWriter();\n let sourceError: unknown;\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n await writer.write(value);\n }\n } catch (e) {\n if (isStreamReaderReleaseError(e)) return;\n sourceError = e;\n } finally {\n if (sourceError) {\n writer.abort(sourceError);\n return;\n }\n\n writer.releaseLock();\n\n try {\n await transform.writable.close();\n } catch {\n // ignore \"WritableStream is closed\" errors\n }\n }\n };\n\n pump();\n\n return transform.readable;\n }\n\n onAttached(): void {\n this.source.onAttached();\n }\n\n onDetached(): void {\n this.source.onDetached();\n }\n}\n\nclass RecorderAudioOutput extends AudioOutput {\n private recorderIO: RecorderIO;\n private writeFn: (buf: AudioFrame[]) => void;\n private accFrames: AudioFrame[] = [];\n private _startedWallTime?: number;\n\n // Pause tracking\n private currentPauseStart?: number;\n private pauseWallTimes: Array<[number, number]> = []; // [start, end] pairs\n\n constructor(\n recorderIO: RecorderIO,\n audioOutput: AudioOutput,\n writeFn: (buf: AudioFrame[]) => void,\n ) {\n super(audioOutput.sampleRate, audioOutput, { pause: true });\n this.recorderIO = recorderIO;\n this.writeFn = writeFn;\n }\n\n get startedWallTime(): number | undefined {\n return this._startedWallTime;\n }\n\n get hasPendingData(): boolean {\n return this.accFrames.length > 0;\n }\n\n pause(): void {\n if (this.currentPauseStart === undefined && this.recorderIO.recording) {\n this.currentPauseStart = Date.now();\n }\n\n if (this.nextInChain) {\n this.nextInChain.pause();\n }\n }\n\n /**\n * Resume playback and record the pause interval\n */\n resume(): void {\n if (this.currentPauseStart !== undefined && this.recorderIO.recording) {\n this.pauseWallTimes.push([this.currentPauseStart, Date.now()]);\n this.currentPauseStart = undefined;\n }\n\n if (this.nextInChain) {\n this.nextInChain.resume();\n }\n }\n\n private resetPauseState(): void {\n this.currentPauseStart = undefined;\n this.pauseWallTimes = [];\n }\n\n onPlaybackFinished(options: PlaybackFinishedEvent): void {\n const finishTime = Date.now();\n\n super.onPlaybackFinished(options);\n\n if (!this.recorderIO.recording) {\n return;\n }\n\n if (this.currentPauseStart !== undefined) {\n this.pauseWallTimes.push([this.currentPauseStart, finishTime]);\n this.currentPauseStart = undefined;\n }\n\n if (this.accFrames.length === 0) {\n this.resetPauseState();\n return;\n }\n\n const playbackPosition = options.playbackPosition;\n\n const pauseEvents: Array<[number, number]> = [];\n\n if (this.pauseWallTimes.length > 0) {\n const totalPauseDuration = this.pauseWallTimes.reduce(\n (sum, [start, end]) => sum + (end - start),\n 0,\n );\n // Convert playbackPosition from seconds to milliseconds for wall time calculations\n const playbackStartTime = finishTime - playbackPosition * 1000 - totalPauseDuration;\n\n let accumulatedPause = 0;\n for (const [pauseStart, pauseEnd] of this.pauseWallTimes) {\n let position = (pauseStart - playbackStartTime - accumulatedPause) / 1000; // Convert to seconds\n const duration = (pauseEnd - pauseStart) / 1000; // Convert to seconds\n position = Math.max(0, Math.min(position, playbackPosition));\n pauseEvents.push([position, duration]);\n accumulatedPause += pauseEnd - pauseStart;\n }\n }\n\n const buf: AudioFrame[] = [];\n let accDur = 0;\n const sampleRate = this.accFrames[0]!.sampleRate;\n const numChannels = this.accFrames[0]!.channels;\n\n let pauseIdx = 0;\n let shouldBreak = false;\n\n for (const frame of this.accFrames) {\n let currentFrame = frame;\n const frameDuration = frame.samplesPerChannel / frame.sampleRate;\n\n if (frameDuration + accDur > playbackPosition) {\n const [left] = splitFrame(currentFrame, playbackPosition - accDur);\n currentFrame = left;\n shouldBreak = true;\n }\n\n // Process any pauses before this frame starts\n while (pauseIdx < pauseEvents.length && pauseEvents[pauseIdx]![0] <= accDur) {\n const [, pauseDur] = pauseEvents[pauseIdx]!;\n buf.push(createSilenceFrame(pauseDur, sampleRate, numChannels));\n pauseIdx++;\n }\n\n // Process any pauses within this frame\n const currentFrameDuration = currentFrame.samplesPerChannel / currentFrame.sampleRate;\n while (\n pauseIdx < pauseEvents.length &&\n pauseEvents[pauseIdx]![0] < accDur + currentFrameDuration\n ) {\n const [pausePos, pauseDur] = pauseEvents[pauseIdx]!;\n const [left, right] = splitFrame(currentFrame, pausePos - accDur);\n buf.push(left);\n accDur += left.samplesPerChannel / left.sampleRate;\n buf.push(createSilenceFrame(pauseDur, sampleRate, numChannels));\n currentFrame = right;\n pauseIdx++;\n }\n\n buf.push(currentFrame);\n accDur += currentFrame.samplesPerChannel / currentFrame.sampleRate;\n\n if (shouldBreak) {\n break;\n }\n }\n\n // Process remaining pauses\n while (pauseIdx < pauseEvents.length) {\n const [pausePos, pauseDur] = pauseEvents[pauseIdx]!;\n if (pausePos <= playbackPosition) {\n buf.push(createSilenceFrame(pauseDur, sampleRate, numChannels));\n }\n pauseIdx++;\n }\n\n if (buf.length > 0) {\n this.writeFn(buf);\n }\n\n this.accFrames = [];\n this.resetPauseState();\n }\n\n async captureFrame(frame: AudioFrame): Promise<void> {\n await super.captureFrame(frame);\n\n if (this.recorderIO.recording) {\n if (this._startedWallTime === undefined) {\n this._startedWallTime = Date.now();\n }\n this.accFrames.push(frame);\n }\n\n if (this.nextInChain) {\n await this.nextInChain.captureFrame(frame);\n }\n }\n\n flush(): void {\n super.flush();\n\n if (this.nextInChain) {\n this.nextInChain.flush();\n }\n }\n\n clearBuffer(): void {\n if (this.nextInChain) {\n this.nextInChain.clearBuffer();\n }\n }\n}\n\n/**\n * Create a silent audio frame with the given duration\n */\nfunction createSilenceFrame(duration: number, sampleRate: number, numChannels: number): AudioFrame {\n const samples = Math.floor(duration * sampleRate);\n const data = new Int16Array(samples * numChannels); // Zero-filled by default\n return new AudioFrame(data, sampleRate, numChannels, samples);\n}\n\n/**\n * Split an audio frame at the given position (in seconds)\n * Returns [left, right] frames\n */\nfunction splitFrame(frame: AudioFrame, position: number): [AudioFrame, AudioFrame] {\n if (position <= 0) {\n const emptyFrame = new AudioFrame(new Int16Array(0), frame.sampleRate, frame.channels, 0);\n return [emptyFrame, frame];\n }\n\n const frameDuration = frame.samplesPerChannel / frame.sampleRate;\n if (position >= frameDuration) {\n const emptyFrame = new AudioFrame(new Int16Array(0), frame.sampleRate, frame.channels, 0);\n return [frame, emptyFrame];\n }\n\n // samplesNeeded is samples per channel (i.e., sample count in time)\n const samplesNeeded = Math.floor(position * frame.sampleRate);\n // Int16Array: each element is one sample, interleaved by channel\n // So total elements = samplesPerChannel * channels\n const numChannels = frame.channels;\n\n const leftData = frame.data.slice(0, samplesNeeded * numChannels);\n const rightData = frame.data.slice(samplesNeeded * numChannels);\n\n const leftFrame = new AudioFrame(leftData, frame.sampleRate, frame.channels, samplesNeeded);\n\n const rightFrame = new AudioFrame(\n rightData,\n frame.sampleRate,\n frame.channels,\n frame.samplesPerChannel - samplesNeeded,\n );\n\n return [leftFrame, rightFrame];\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/voice/recorder_io/recorder_io.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport ffmpegInstaller from '@ffmpeg-installer/ffmpeg';\nimport { Mutex } from '@livekit/mutex';\nimport { AudioFrame, AudioResampler } from '@livekit/rtc-node';\nimport ffmpeg from 'fluent-ffmpeg';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { PassThrough } from 'node:stream';\nimport type { ReadableStream } from 'node:stream/web';\nimport { TransformStream } from 'node:stream/web';\nimport { log } from '../../log.js';\nimport { isStreamReaderReleaseError } from '../../stream/deferred_stream.js';\nimport { type StreamChannel, createStreamChannel } from '../../stream/stream_channel.js';\nimport { Future, Task, cancelAndWait, delay } from '../../utils.js';\nimport type { AgentSession } from '../agent_session.js';\nimport { AudioInput, AudioOutput, type PlaybackFinishedEvent } from '../io.js';\n\nffmpeg.setFfmpegPath(ffmpegInstaller.path);\n\nconst WRITE_INTERVAL_MS = 2500;\nconst DEFAULT_SAMPLE_RATE = 48000;\n\nexport interface RecorderOptions {\n agentSession: AgentSession;\n sampleRate?: number;\n}\n\ninterface ResampleAndMixOptions {\n frames: AudioFrame[];\n resampler: AudioResampler | undefined;\n flush?: boolean;\n}\n\nexport class RecorderIO {\n private inRecord?: RecorderAudioInput;\n private outRecord?: RecorderAudioOutput;\n\n private inChan: StreamChannel<AudioFrame[]> = createStreamChannel<AudioFrame[]>();\n private outChan: StreamChannel<AudioFrame[]> = createStreamChannel<AudioFrame[]>();\n\n private session: AgentSession;\n private sampleRate: number;\n\n private _outputPath?: string;\n private forwardTask?: Task<void>;\n private encodeTask?: Task<void>;\n\n private closeFuture: Future<void> = new Future();\n private lock: Mutex = new Mutex();\n private started: boolean = false;\n\n // FFmpeg streaming state\n private pcmStream?: PassThrough;\n private ffmpegPromise?: Promise<void>;\n private inResampler?: AudioResampler;\n private outResampler?: AudioResampler;\n\n private logger = log();\n\n constructor(opts: RecorderOptions) {\n const { agentSession, sampleRate = DEFAULT_SAMPLE_RATE } = opts;\n\n this.session = agentSession;\n this.sampleRate = sampleRate;\n }\n\n async start(outputPath: string): Promise<void> {\n const unlock = await this.lock.lock();\n\n try {\n if (this.started) return;\n\n if (!this.inRecord || !this.outRecord) {\n throw new Error(\n 'RecorderIO not properly initialized: both `recordInput()` and `recordOutput()` must be called before starting the recorder.',\n );\n }\n\n this._outputPath = outputPath;\n this.started = true;\n this.closeFuture = new Future();\n\n // Ensure output directory exists\n const dir = path.dirname(outputPath);\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true });\n }\n\n this.forwardTask = Task.from(({ signal }) => this.forward(signal));\n this.encodeTask = Task.from(() => this.encode(), undefined, 'recorder_io_encode_task');\n } finally {\n unlock();\n }\n }\n\n async close(): Promise<void> {\n const unlock = await this.lock.lock();\n\n try {\n if (!this.started) return;\n\n await this.inChan.close();\n await this.outChan.close();\n await this.closeFuture.await;\n await cancelAndWait([this.forwardTask!, this.encodeTask!]);\n\n this.started = false;\n } finally {\n unlock();\n }\n }\n\n recordInput(audioInput: AudioInput): RecorderAudioInput {\n this.inRecord = new RecorderAudioInput(this, audioInput);\n return this.inRecord;\n }\n\n recordOutput(audioOutput: AudioOutput): RecorderAudioOutput {\n this.outRecord = new RecorderAudioOutput(this, audioOutput, (buf) => this.writeCb(buf));\n return this.outRecord;\n }\n\n private writeCb(buf: AudioFrame[]): void {\n const inputBuf = this.inRecord!.takeBuf(this.outRecord?._lastSpeechEndTime);\n this.inChan.write(inputBuf);\n this.outChan.write(buf);\n }\n\n get recording(): boolean {\n return this.started;\n }\n\n get outputPath(): string | undefined {\n return this._outputPath;\n }\n\n get recordingStartedAt(): number | undefined {\n const inT = this.inRecord?.startedWallTime;\n const outT = this.outRecord?.startedWallTime;\n\n if (inT === undefined) {\n return outT;\n }\n\n if (outT === undefined) {\n return inT;\n }\n\n return Math.min(inT, outT);\n }\n\n /**\n * Forward task: periodically flush input buffer to encoder\n */\n private async forward(signal: AbortSignal): Promise<void> {\n while (!signal.aborted) {\n try {\n await delay(WRITE_INTERVAL_MS, { signal });\n } catch {\n // Aborted\n break;\n }\n\n if (this.outRecord!.hasPendingData) {\n // If the output is currently playing audio, wait for it to stay in sync\n continue;\n }\n\n // Flush input buffer\n const inputBuf = this.inRecord!.takeBuf(this.outRecord!._lastSpeechEndTime);\n this.inChan\n .write(inputBuf)\n .catch((err) => this.logger.error({ err }, 'Error writing RecorderIO input buffer'));\n this.outChan\n .write([])\n .catch((err) => this.logger.error({ err }, 'Error writing RecorderIO output buffer'));\n }\n }\n\n /**\n * Start FFmpeg process for streaming encoding\n */\n private startFFmpeg(): void {\n if (this.pcmStream) return;\n\n this.pcmStream = new PassThrough();\n\n this.ffmpegPromise = new Promise<void>((resolve, reject) => {\n ffmpeg(this.pcmStream!)\n .inputFormat('s16le')\n .inputOptions([`-ar ${this.sampleRate}`, '-ac 2'])\n .audioCodec('libopus')\n .audioChannels(2)\n .audioFrequency(this.sampleRate)\n .format('ogg')\n .output(this._outputPath!)\n .on('end', () => {\n this.logger.debug('FFmpeg encoding finished');\n resolve();\n })\n .on('error', (err) => {\n // Ignore errors from intentional stream closure or SIGINT during shutdown\n if (\n err.message?.includes('Output stream closed') ||\n err.message?.includes('received signal 2') ||\n err.message?.includes('SIGKILL') ||\n err.message?.includes('SIGINT')\n ) {\n resolve();\n } else {\n this.logger.error({ err }, 'FFmpeg encoding error');\n reject(err);\n }\n })\n .run();\n });\n }\n\n /**\n * Resample and mix frames to mono Float32\n */\n private resampleAndMix(opts: ResampleAndMixOptions): {\n samples: Float32Array;\n resampler: AudioResampler | undefined;\n } {\n const INV_INT16 = 1.0 / 32768.0;\n const { frames, flush = false } = opts;\n let { resampler } = opts;\n\n if (frames.length === 0 && !flush) {\n return { samples: new Float32Array(0), resampler };\n }\n\n if (!resampler && frames.length > 0) {\n const firstFrame = frames[0]!;\n resampler = new AudioResampler(firstFrame.sampleRate, this.sampleRate, firstFrame.channels);\n }\n\n const resampledFrames: AudioFrame[] = [];\n for (const frame of frames) {\n if (resampler) {\n resampledFrames.push(...resampler.push(frame));\n }\n }\n\n if (flush && resampler) {\n resampledFrames.push(...resampler.flush());\n }\n\n const totalSamples = resampledFrames.reduce((acc, frame) => acc + frame.samplesPerChannel, 0);\n const samples = new Float32Array(totalSamples);\n\n let pos = 0;\n for (const frame of resampledFrames) {\n const data = frame.data;\n const numChannels = frame.channels;\n for (let i = 0; i < frame.samplesPerChannel; i++) {\n let sum = 0;\n for (let ch = 0; ch < numChannels; ch++) {\n sum += data[i * numChannels + ch]!;\n }\n samples[pos++] = (sum / numChannels) * INV_INT16;\n }\n }\n\n return { samples, resampler };\n }\n\n /**\n * Write PCM chunk to FFmpeg stream\n */\n private writePCM(leftSamples: Float32Array, rightSamples: Float32Array): void {\n if (!this.pcmStream) {\n this.startFFmpeg();\n }\n\n // Handle length mismatch by prepending silence\n if (leftSamples.length !== rightSamples.length) {\n const diff = Math.abs(leftSamples.length - rightSamples.length);\n if (leftSamples.length < rightSamples.length) {\n this.logger.warn(\n `Input is shorter by ${diff} samples; silence has been prepended to align the input channel.`,\n );\n const padded = new Float32Array(rightSamples.length);\n padded.set(leftSamples, diff);\n leftSamples = padded;\n } else {\n const padded = new Float32Array(leftSamples.length);\n padded.set(rightSamples, diff);\n rightSamples = padded;\n }\n }\n\n const maxLen = Math.max(leftSamples.length, rightSamples.length);\n if (maxLen <= 0) return;\n\n // Interleave stereo samples and convert back to Int16\n const stereoData = new Int16Array(maxLen * 2);\n for (let i = 0; i < maxLen; i++) {\n stereoData[i * 2] = Math.max(\n -32768,\n Math.min(32767, Math.round((leftSamples[i] ?? 0) * 32768)),\n );\n stereoData[i * 2 + 1] = Math.max(\n -32768,\n Math.min(32767, Math.round((rightSamples[i] ?? 0) * 32768)),\n );\n }\n\n this.pcmStream!.write(Buffer.from(stereoData.buffer));\n }\n\n /**\n * Encode task: read from channels, mix to stereo, stream to FFmpeg\n */\n private async encode(): Promise<void> {\n if (!this._outputPath) return;\n\n const inReader = this.inChan.stream().getReader();\n const outReader = this.outChan.stream().getReader();\n\n try {\n while (true) {\n const [inResult, outResult] = await Promise.all([inReader.read(), outReader.read()]);\n\n if (inResult.done || outResult.done) {\n break;\n }\n\n const inputBuf = inResult.value;\n const outputBuf = outResult.value;\n\n const inMixed = this.resampleAndMix({ frames: inputBuf, resampler: this.inResampler });\n this.inResampler = inMixed.resampler;\n\n const outMixed = this.resampleAndMix({\n frames: outputBuf,\n resampler: this.outResampler,\n flush: outputBuf.length > 0,\n });\n this.outResampler = outMixed.resampler;\n\n // Stream PCM data directly to FFmpeg\n this.writePCM(inMixed.samples, outMixed.samples);\n }\n\n // Close FFmpeg stream and wait for encoding to complete\n if (this.pcmStream) {\n this.pcmStream.end();\n await this.ffmpegPromise;\n }\n } catch (err) {\n this.logger.error({ err }, 'Error in encode task');\n } finally {\n inReader.releaseLock();\n outReader.releaseLock();\n\n if (!this.closeFuture.done) {\n this.closeFuture.resolve();\n }\n }\n }\n}\n\nclass RecorderAudioInput extends AudioInput {\n private source: AudioInput;\n private recorderIO: RecorderIO;\n private accFrames: AudioFrame[] = [];\n private _startedWallTime?: number;\n private _padded: boolean = false;\n private logger = log();\n\n constructor(recorderIO: RecorderIO, source: AudioInput) {\n super();\n this.recorderIO = recorderIO;\n this.source = source;\n\n // Set up the intercepting stream\n this.deferredStream.setSource(this.createInterceptingStream());\n }\n\n /**\n * Wall-clock time when the first frame was captured\n */\n get startedWallTime(): number | undefined {\n return this._startedWallTime;\n }\n\n /**\n * Take accumulated frames and clear the buffer\n * @param padSince - If provided and input started after this time, pad with silence\n */\n takeBuf(padSince?: number): AudioFrame[] {\n let frames = this.accFrames;\n this.accFrames = [];\n\n if (\n padSince !== undefined &&\n this._startedWallTime !== undefined &&\n this._startedWallTime > padSince &&\n !this._padded &&\n frames.length > 0\n ) {\n const padding = this._startedWallTime - padSince;\n this.logger.warn(\n {\n lastAgentSpeechTime: padSince,\n inputStartedTime: this._startedWallTime,\n },\n 'input speech started after last agent speech ended',\n );\n this._padded = true;\n const firstFrame = frames[0]!;\n frames = [\n createSilenceFrame(padding / 1000, firstFrame.sampleRate, firstFrame.channels),\n ...frames,\n ];\n } else if (\n padSince !== undefined &&\n this._startedWallTime === undefined &&\n !this._padded &&\n frames.length === 0\n ) {\n // We could pad with silence here with some fixed SR and channels,\n // but it's better for the user to know that this is happening\n this.logger.warn(\n \"input speech hasn't started yet, skipping silence padding, recording may be inaccurate until the speech starts\",\n );\n }\n\n return frames;\n }\n\n /**\n * Creates a stream that intercepts frames from the source,\n * accumulates them when recording, and passes them through unchanged.\n */\n private createInterceptingStream(): ReadableStream<AudioFrame> {\n const sourceStream = this.source.stream;\n const reader = sourceStream.getReader();\n\n const transform = new TransformStream<AudioFrame, AudioFrame>({\n transform: (frame, controller) => {\n // Accumulate frames when recording is active\n if (this.recorderIO.recording) {\n if (this._startedWallTime === undefined) {\n this._startedWallTime = Date.now();\n }\n this.accFrames.push(frame);\n }\n\n controller.enqueue(frame);\n },\n });\n\n const pump = async () => {\n const writer = transform.writable.getWriter();\n let sourceError: unknown;\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n await writer.write(value);\n }\n } catch (e) {\n if (isStreamReaderReleaseError(e)) return;\n sourceError = e;\n } finally {\n if (sourceError) {\n writer.abort(sourceError);\n return;\n }\n\n writer.releaseLock();\n\n try {\n await transform.writable.close();\n } catch {\n // ignore \"WritableStream is closed\" errors\n }\n }\n };\n\n pump();\n\n return transform.readable;\n }\n\n onAttached(): void {\n this.source.onAttached();\n }\n\n onDetached(): void {\n this.source.onDetached();\n }\n}\n\nclass RecorderAudioOutput extends AudioOutput {\n private recorderIO: RecorderIO;\n private writeFn: (buf: AudioFrame[]) => void;\n private accFrames: AudioFrame[] = [];\n private _startedWallTime?: number;\n private _logger = log();\n\n _lastSpeechEndTime?: number;\n private _lastSpeechStartTime?: number;\n\n // Pause tracking\n private currentPauseStart?: number;\n private pauseWallTimes: Array<[number, number]> = []; // [start, end] pairs\n\n constructor(\n recorderIO: RecorderIO,\n audioOutput: AudioOutput,\n writeFn: (buf: AudioFrame[]) => void,\n ) {\n super(audioOutput.sampleRate, audioOutput, { pause: true });\n this.recorderIO = recorderIO;\n this.writeFn = writeFn;\n }\n\n get startedWallTime(): number | undefined {\n return this._startedWallTime;\n }\n\n get hasPendingData(): boolean {\n return this.accFrames.length > 0;\n }\n\n pause(): void {\n if (this.currentPauseStart === undefined && this.recorderIO.recording) {\n this.currentPauseStart = Date.now();\n }\n\n if (this.nextInChain) {\n this.nextInChain.pause();\n }\n }\n\n /**\n * Resume playback and record the pause interval\n */\n resume(): void {\n if (this.currentPauseStart !== undefined && this.recorderIO.recording) {\n this.pauseWallTimes.push([this.currentPauseStart, Date.now()]);\n this.currentPauseStart = undefined;\n }\n\n if (this.nextInChain) {\n this.nextInChain.resume();\n }\n }\n\n private resetPauseState(): void {\n this.currentPauseStart = undefined;\n this.pauseWallTimes = [];\n }\n\n onPlaybackFinished(options: PlaybackFinishedEvent): void {\n const finishTime = this.currentPauseStart ?? Date.now();\n const trailingSilenceDuration = Math.max(0, Date.now() - finishTime);\n\n // Convert playbackPosition from seconds to ms for internal calculations\n let playbackPosition = options.playbackPosition * 1000;\n\n if (this._lastSpeechStartTime === undefined) {\n this._logger.warn(\n {\n finishTime,\n playbackPosition,\n interrupted: options.interrupted,\n },\n 'playback finished before speech started',\n );\n playbackPosition = 0;\n }\n\n // Clamp playbackPosition to actual elapsed time (all in ms)\n playbackPosition = Math.max(\n 0,\n Math.min(finishTime - (this._lastSpeechStartTime ?? 0), playbackPosition),\n );\n\n // Convert back to seconds for the event\n super.onPlaybackFinished({ ...options, playbackPosition: playbackPosition / 1000 });\n\n if (!this.recorderIO.recording) {\n return;\n }\n\n if (this.currentPauseStart !== undefined) {\n this.pauseWallTimes.push([this.currentPauseStart, finishTime]);\n this.currentPauseStart = undefined;\n }\n\n if (this.accFrames.length === 0) {\n this.resetPauseState();\n this._lastSpeechEndTime = Date.now();\n this._lastSpeechStartTime = undefined;\n return;\n }\n\n // pauseEvents stores (position, duration) in ms\n const pauseEvents: Array<[number, number]> = [];\n let playbackStartTime = finishTime - playbackPosition;\n\n if (this.pauseWallTimes.length > 0) {\n const totalPauseDuration = this.pauseWallTimes.reduce(\n (sum, [start, end]) => sum + (end - start),\n 0,\n );\n playbackStartTime = finishTime - playbackPosition - totalPauseDuration;\n\n let accumulatedPause = 0;\n for (const [pauseStart, pauseEnd] of this.pauseWallTimes) {\n let position = pauseStart - playbackStartTime - accumulatedPause;\n const duration = pauseEnd - pauseStart;\n position = Math.max(0, Math.min(position, playbackPosition));\n pauseEvents.push([position, duration]);\n accumulatedPause += duration;\n }\n }\n\n const buf: AudioFrame[] = [];\n let accDur = 0;\n const sampleRate = this.accFrames[0]!.sampleRate;\n const numChannels = this.accFrames[0]!.channels;\n\n let pauseIdx = 0;\n let shouldBreak = false;\n\n for (const frame of this.accFrames) {\n let currentFrame = frame;\n const frameDuration = (frame.samplesPerChannel / frame.sampleRate) * 1000;\n\n if (frameDuration + accDur > playbackPosition) {\n const [left] = splitFrame(currentFrame, (playbackPosition - accDur) / 1000);\n currentFrame = left;\n shouldBreak = true;\n }\n\n // Process any pauses before this frame starts\n while (pauseIdx < pauseEvents.length && pauseEvents[pauseIdx]![0] <= accDur) {\n const [, pauseDur] = pauseEvents[pauseIdx]!;\n buf.push(createSilenceFrame(pauseDur / 1000, sampleRate, numChannels));\n pauseIdx++;\n }\n\n // Process any pauses within this frame\n const currentFrameDuration =\n (currentFrame.samplesPerChannel / currentFrame.sampleRate) * 1000;\n while (\n pauseIdx < pauseEvents.length &&\n pauseEvents[pauseIdx]![0] < accDur + currentFrameDuration\n ) {\n const [pausePos, pauseDur] = pauseEvents[pauseIdx]!;\n const [left, right] = splitFrame(currentFrame, (pausePos - accDur) / 1000);\n buf.push(left);\n accDur += (left.samplesPerChannel / left.sampleRate) * 1000;\n buf.push(createSilenceFrame(pauseDur / 1000, sampleRate, numChannels));\n\n currentFrame = right;\n pauseIdx++;\n }\n\n buf.push(currentFrame);\n accDur += (currentFrame.samplesPerChannel / currentFrame.sampleRate) * 1000;\n\n if (shouldBreak) {\n break;\n }\n }\n\n // Process remaining pauses\n while (pauseIdx < pauseEvents.length) {\n const [pausePos, pauseDur] = pauseEvents[pauseIdx]!;\n if (pausePos <= playbackPosition) {\n buf.push(createSilenceFrame(pauseDur / 1000, sampleRate, numChannels));\n }\n pauseIdx++;\n }\n\n if (buf.length > 0) {\n if (trailingSilenceDuration > 0) {\n buf.push(createSilenceFrame(trailingSilenceDuration / 1000, sampleRate, numChannels));\n }\n this.writeFn(buf);\n }\n\n this.accFrames = [];\n this.resetPauseState();\n this._lastSpeechEndTime = Date.now();\n this._lastSpeechStartTime = undefined;\n }\n\n async captureFrame(frame: AudioFrame): Promise<void> {\n if (this.nextInChain) {\n await this.nextInChain.captureFrame(frame);\n }\n\n await super.captureFrame(frame);\n\n if (this.recorderIO.recording) {\n this.accFrames.push(frame);\n }\n\n if (this._startedWallTime === undefined) {\n this._startedWallTime = Date.now();\n }\n\n if (this._lastSpeechStartTime === undefined) {\n this._lastSpeechStartTime = Date.now();\n }\n }\n\n flush(): void {\n super.flush();\n\n if (this.nextInChain) {\n this.nextInChain.flush();\n }\n }\n\n clearBuffer(): void {\n if (this.nextInChain) {\n this.nextInChain.clearBuffer();\n }\n }\n}\n\n/**\n * Create a silent audio frame with the given duration\n */\nfunction createSilenceFrame(\n durationInS: number,\n sampleRate: number,\n numChannels: number,\n): AudioFrame {\n const samples = Math.floor(durationInS * sampleRate);\n const data = new Int16Array(samples * numChannels); // Zero-filled by default\n return new AudioFrame(data, sampleRate, numChannels, samples);\n}\n\n/**\n * Split an audio frame at the given position (in seconds)\n * Returns [left, right] frames\n */\nfunction splitFrame(frame: AudioFrame, position: number): [AudioFrame, AudioFrame] {\n if (position <= 0) {\n const emptyFrame = new AudioFrame(new Int16Array(0), frame.sampleRate, frame.channels, 0);\n return [emptyFrame, frame];\n }\n\n const frameDuration = frame.samplesPerChannel / frame.sampleRate;\n if (position >= frameDuration) {\n const emptyFrame = new AudioFrame(new Int16Array(0), frame.sampleRate, frame.channels, 0);\n return [frame, emptyFrame];\n }\n\n // samplesNeeded is samples per channel (i.e., sample count in time)\n const samplesNeeded = Math.floor(position * frame.sampleRate);\n // Int16Array: each element is one sample, interleaved by channel\n // So total elements = samplesPerChannel * channels\n const numChannels = frame.channels;\n\n const leftData = frame.data.slice(0, samplesNeeded * numChannels);\n const rightData = frame.data.slice(samplesNeeded * numChannels);\n\n const leftFrame = new AudioFrame(leftData, frame.sampleRate, frame.channels, samplesNeeded);\n\n const rightFrame = new AudioFrame(\n rightData,\n frame.sampleRate,\n frame.channels,\n frame.samplesPerChannel - samplesNeeded,\n );\n\n return [leftFrame, rightFrame];\n}\n"],"mappings":"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","names":[]}
|
|
@@ -22,6 +22,7 @@ __export(input_exports, {
|
|
|
22
22
|
});
|
|
23
23
|
module.exports = __toCommonJS(input_exports);
|
|
24
24
|
var import_rtc_node = require("@livekit/rtc-node");
|
|
25
|
+
var import_rtc_node2 = require("@livekit/rtc-node");
|
|
25
26
|
var import_log = require("../../log.cjs");
|
|
26
27
|
var import_utils = require("../../utils.cjs");
|
|
27
28
|
var import_io = require("../io.cjs");
|
|
@@ -30,6 +31,7 @@ class ParticipantAudioInputStream extends import_io.AudioInput {
|
|
|
30
31
|
sampleRate;
|
|
31
32
|
numChannels;
|
|
32
33
|
noiseCancellation;
|
|
34
|
+
frameProcessor;
|
|
33
35
|
publication = null;
|
|
34
36
|
participantIdentity = null;
|
|
35
37
|
logger = (0, import_log.log)();
|
|
@@ -43,13 +45,18 @@ class ParticipantAudioInputStream extends import_io.AudioInput {
|
|
|
43
45
|
this.room = room;
|
|
44
46
|
this.sampleRate = sampleRate;
|
|
45
47
|
this.numChannels = numChannels;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
if (noiseCancellation instanceof import_rtc_node.FrameProcessor) {
|
|
49
|
+
this.frameProcessor = noiseCancellation;
|
|
50
|
+
} else {
|
|
51
|
+
this.noiseCancellation = noiseCancellation;
|
|
52
|
+
}
|
|
53
|
+
this.room.on(import_rtc_node2.RoomEvent.TrackSubscribed, this.onTrackSubscribed);
|
|
54
|
+
this.room.on(import_rtc_node2.RoomEvent.TrackUnpublished, this.onTrackUnpublished);
|
|
55
|
+
this.room.on(import_rtc_node2.RoomEvent.TokenRefreshed, this.onTokenRefreshed);
|
|
49
56
|
}
|
|
50
57
|
setParticipant(participant) {
|
|
51
58
|
this.logger.debug({ participant }, "setting participant audio input");
|
|
52
|
-
const participantIdentity = participant instanceof
|
|
59
|
+
const participantIdentity = participant instanceof import_rtc_node2.RemoteParticipant ? participant.identity : participant;
|
|
53
60
|
if (this.participantIdentity === participantIdentity) {
|
|
54
61
|
return;
|
|
55
62
|
}
|
|
@@ -58,7 +65,7 @@ class ParticipantAudioInputStream extends import_io.AudioInput {
|
|
|
58
65
|
if (!participantIdentity) {
|
|
59
66
|
return;
|
|
60
67
|
}
|
|
61
|
-
const participantValue = participant instanceof
|
|
68
|
+
const participantValue = participant instanceof import_rtc_node2.RemoteParticipant ? participant : this.room.remoteParticipants.get(participantIdentity);
|
|
62
69
|
const trackPublicationsArray = Array.from((participantValue == null ? void 0 : participantValue.trackPublications.values()) ?? []);
|
|
63
70
|
this.logger.info(
|
|
64
71
|
{
|
|
@@ -70,7 +77,7 @@ class ParticipantAudioInputStream extends import_io.AudioInput {
|
|
|
70
77
|
);
|
|
71
78
|
if (participantValue) {
|
|
72
79
|
for (const publication of participantValue.trackPublications.values()) {
|
|
73
|
-
if (publication.track && publication.source ===
|
|
80
|
+
if (publication.track && publication.source === import_rtc_node2.TrackSource.SOURCE_MICROPHONE) {
|
|
74
81
|
this.onTrackSubscribed(publication.track, publication, participantValue);
|
|
75
82
|
break;
|
|
76
83
|
}
|
|
@@ -90,14 +97,17 @@ class ParticipantAudioInputStream extends import_io.AudioInput {
|
|
|
90
97
|
}
|
|
91
98
|
};
|
|
92
99
|
closeStream() {
|
|
100
|
+
var _a;
|
|
93
101
|
if (this.deferredStream.isSourceSet) {
|
|
94
102
|
this.deferredStream.detachSource();
|
|
95
103
|
}
|
|
104
|
+
(_a = this.frameProcessor) == null ? void 0 : _a.close();
|
|
96
105
|
this.publication = null;
|
|
97
106
|
}
|
|
98
107
|
onTrackSubscribed = (track, publication, participant) => {
|
|
108
|
+
var _a, _b;
|
|
99
109
|
this.logger.debug({ participant: participant.identity }, "onTrackSubscribed in _input");
|
|
100
|
-
if (this.participantIdentity !== participant.identity || publication.source !==
|
|
110
|
+
if (this.participantIdentity !== participant.identity || publication.source !== import_rtc_node2.TrackSource.SOURCE_MICROPHONE || this.publication && this.publication.sid === publication.sid) {
|
|
101
111
|
return false;
|
|
102
112
|
}
|
|
103
113
|
this.closeStream();
|
|
@@ -108,19 +118,38 @@ class ParticipantAudioInputStream extends import_io.AudioInput {
|
|
|
108
118
|
outputRate: this.sampleRate
|
|
109
119
|
})
|
|
110
120
|
);
|
|
121
|
+
(_a = this.frameProcessor) == null ? void 0 : _a.onStreamInfoUpdated({
|
|
122
|
+
participantIdentity: participant.identity,
|
|
123
|
+
roomName: this.room.name,
|
|
124
|
+
publicationSid: publication.sid
|
|
125
|
+
});
|
|
126
|
+
(_b = this.frameProcessor) == null ? void 0 : _b.onCredentialsUpdated({
|
|
127
|
+
token: this.room.token,
|
|
128
|
+
url: this.room.serverUrl
|
|
129
|
+
});
|
|
111
130
|
return true;
|
|
112
131
|
};
|
|
132
|
+
onTokenRefreshed = () => {
|
|
133
|
+
var _a;
|
|
134
|
+
if (this.room.token && this.room.serverUrl) {
|
|
135
|
+
(_a = this.frameProcessor) == null ? void 0 : _a.onCredentialsUpdated({
|
|
136
|
+
token: this.room.token,
|
|
137
|
+
url: this.room.serverUrl
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
};
|
|
113
141
|
createStream(track) {
|
|
114
|
-
return new
|
|
142
|
+
return new import_rtc_node2.AudioStream(track, {
|
|
115
143
|
sampleRate: this.sampleRate,
|
|
116
144
|
numChannels: this.numChannels,
|
|
117
|
-
noiseCancellation: this.noiseCancellation
|
|
145
|
+
noiseCancellation: this.frameProcessor || this.noiseCancellation
|
|
118
146
|
// TODO(AJS-269): resolve compatibility issue with node-sdk to remove the forced type casting
|
|
119
147
|
});
|
|
120
148
|
}
|
|
121
149
|
async close() {
|
|
122
|
-
this.room.off(
|
|
123
|
-
this.room.off(
|
|
150
|
+
this.room.off(import_rtc_node2.RoomEvent.TrackSubscribed, this.onTrackSubscribed);
|
|
151
|
+
this.room.off(import_rtc_node2.RoomEvent.TrackUnpublished, this.onTrackUnpublished);
|
|
152
|
+
this.room.off(import_rtc_node2.RoomEvent.TokenRefreshed, this.onTokenRefreshed);
|
|
124
153
|
this.closeStream();
|
|
125
154
|
await this.deferredStream.stream.cancel().catch(() => {
|
|
126
155
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/voice/room_io/_input.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport type
|
|
1
|
+
{"version":3,"sources":["../../../src/voice/room_io/_input.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport { type AudioFrame, FrameProcessor } from '@livekit/rtc-node';\nimport {\n AudioStream,\n type NoiseCancellationOptions,\n RemoteParticipant,\n type RemoteTrack,\n type RemoteTrackPublication,\n type Room,\n RoomEvent,\n TrackSource,\n} from '@livekit/rtc-node';\nimport type { ReadableStream } from 'node:stream/web';\nimport { log } from '../../log.js';\nimport { resampleStream } from '../../utils.js';\nimport { AudioInput } from '../io.js';\n\nexport class ParticipantAudioInputStream extends AudioInput {\n private room: Room;\n private sampleRate: number;\n private numChannels: number;\n private noiseCancellation?: NoiseCancellationOptions;\n private frameProcessor?: FrameProcessor<AudioFrame>;\n private publication: RemoteTrackPublication | null = null;\n private participantIdentity: string | null = null;\n private logger = log();\n constructor({\n room,\n sampleRate,\n numChannels,\n noiseCancellation,\n }: {\n room: Room;\n sampleRate: number;\n numChannels: number;\n noiseCancellation?: NoiseCancellationOptions | FrameProcessor<AudioFrame>;\n }) {\n super();\n this.room = room;\n this.sampleRate = sampleRate;\n this.numChannels = numChannels;\n if (noiseCancellation instanceof FrameProcessor) {\n this.frameProcessor = noiseCancellation;\n } else {\n this.noiseCancellation = noiseCancellation;\n }\n\n this.room.on(RoomEvent.TrackSubscribed, this.onTrackSubscribed);\n this.room.on(RoomEvent.TrackUnpublished, this.onTrackUnpublished);\n this.room.on(RoomEvent.TokenRefreshed, this.onTokenRefreshed);\n }\n\n setParticipant(participant: RemoteParticipant | string | null) {\n this.logger.debug({ participant }, 'setting participant audio input');\n const participantIdentity =\n participant instanceof RemoteParticipant ? participant.identity : participant;\n\n if (this.participantIdentity === participantIdentity) {\n return;\n }\n this.participantIdentity = participantIdentity;\n this.closeStream();\n\n if (!participantIdentity) {\n return;\n }\n\n const participantValue =\n participant instanceof RemoteParticipant\n ? participant\n : this.room.remoteParticipants.get(participantIdentity);\n\n // Convert Map iterator to array for Pino serialization\n const trackPublicationsArray = Array.from(participantValue?.trackPublications.values() ?? []);\n\n this.logger.info(\n {\n participantValue: participantValue?.identity,\n trackPublications: trackPublicationsArray,\n lengthOfTrackPublications: trackPublicationsArray.length,\n },\n 'participantValue.trackPublications',\n );\n // We need to check if the participant has a microphone track and subscribe to it\n // in case we miss the tracksubscribed event\n if (participantValue) {\n for (const publication of participantValue.trackPublications.values()) {\n if (publication.track && publication.source === TrackSource.SOURCE_MICROPHONE) {\n this.onTrackSubscribed(publication.track, publication, participantValue);\n break;\n }\n }\n }\n }\n\n private onTrackUnpublished = (\n publication: RemoteTrackPublication,\n participant: RemoteParticipant,\n ) => {\n if (\n this.publication?.sid !== publication.sid ||\n participant.identity !== this.participantIdentity\n ) {\n return;\n }\n this.closeStream();\n\n // subscribe to the first available track\n for (const publication of participant.trackPublications.values()) {\n if (\n publication.track &&\n this.onTrackSubscribed(publication.track, publication, participant)\n ) {\n return;\n }\n }\n };\n\n private closeStream() {\n if (this.deferredStream.isSourceSet) {\n this.deferredStream.detachSource();\n }\n\n this.frameProcessor?.close();\n\n this.publication = null;\n }\n\n private onTrackSubscribed = (\n track: RemoteTrack,\n publication: RemoteTrackPublication,\n participant: RemoteParticipant,\n ): boolean => {\n this.logger.debug({ participant: participant.identity }, 'onTrackSubscribed in _input');\n if (\n this.participantIdentity !== participant.identity ||\n publication.source !== TrackSource.SOURCE_MICROPHONE ||\n (this.publication && this.publication.sid === publication.sid)\n ) {\n return false;\n }\n this.closeStream();\n this.publication = publication;\n this.deferredStream.setSource(\n resampleStream({\n stream: this.createStream(track),\n outputRate: this.sampleRate,\n }),\n );\n this.frameProcessor?.onStreamInfoUpdated({\n participantIdentity: participant.identity,\n roomName: this.room.name!,\n publicationSid: publication.sid!,\n });\n this.frameProcessor?.onCredentialsUpdated({\n token: this.room.token!,\n url: this.room.serverUrl!,\n });\n return true;\n };\n\n private onTokenRefreshed = () => {\n if (this.room.token && this.room.serverUrl) {\n this.frameProcessor?.onCredentialsUpdated({\n token: this.room.token,\n url: this.room.serverUrl,\n });\n }\n };\n\n private createStream(track: RemoteTrack): ReadableStream<AudioFrame> {\n return new AudioStream(track, {\n sampleRate: this.sampleRate,\n numChannels: this.numChannels,\n noiseCancellation: this.frameProcessor || this.noiseCancellation,\n // TODO(AJS-269): resolve compatibility issue with node-sdk to remove the forced type casting\n }) as unknown as ReadableStream<AudioFrame>;\n }\n\n async close() {\n this.room.off(RoomEvent.TrackSubscribed, this.onTrackSubscribed);\n this.room.off(RoomEvent.TrackUnpublished, this.onTrackUnpublished);\n this.room.off(RoomEvent.TokenRefreshed, this.onTokenRefreshed);\n this.closeStream();\n // Ignore errors - stream may be locked by RecorderIO or already cancelled\n await this.deferredStream.stream.cancel().catch(() => {});\n }\n}\n"],"mappings":"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","names":["import_rtc_node","publication"]}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type AudioFrame, FrameProcessor } from '@livekit/rtc-node';
|
|
1
2
|
import { type NoiseCancellationOptions, RemoteParticipant, type Room } from '@livekit/rtc-node';
|
|
2
3
|
import { AudioInput } from '../io.js';
|
|
3
4
|
export declare class ParticipantAudioInputStream extends AudioInput {
|
|
@@ -5,6 +6,7 @@ export declare class ParticipantAudioInputStream extends AudioInput {
|
|
|
5
6
|
private sampleRate;
|
|
6
7
|
private numChannels;
|
|
7
8
|
private noiseCancellation?;
|
|
9
|
+
private frameProcessor?;
|
|
8
10
|
private publication;
|
|
9
11
|
private participantIdentity;
|
|
10
12
|
private logger;
|
|
@@ -12,12 +14,13 @@ export declare class ParticipantAudioInputStream extends AudioInput {
|
|
|
12
14
|
room: Room;
|
|
13
15
|
sampleRate: number;
|
|
14
16
|
numChannels: number;
|
|
15
|
-
noiseCancellation?: NoiseCancellationOptions
|
|
17
|
+
noiseCancellation?: NoiseCancellationOptions | FrameProcessor<AudioFrame>;
|
|
16
18
|
});
|
|
17
19
|
setParticipant(participant: RemoteParticipant | string | null): void;
|
|
18
20
|
private onTrackUnpublished;
|
|
19
21
|
private closeStream;
|
|
20
22
|
private onTrackSubscribed;
|
|
23
|
+
private onTokenRefreshed;
|
|
21
24
|
private createStream;
|
|
22
25
|
close(): Promise<void>;
|
|
23
26
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type AudioFrame, FrameProcessor } from '@livekit/rtc-node';
|
|
1
2
|
import { type NoiseCancellationOptions, RemoteParticipant, type Room } from '@livekit/rtc-node';
|
|
2
3
|
import { AudioInput } from '../io.js';
|
|
3
4
|
export declare class ParticipantAudioInputStream extends AudioInput {
|
|
@@ -5,6 +6,7 @@ export declare class ParticipantAudioInputStream extends AudioInput {
|
|
|
5
6
|
private sampleRate;
|
|
6
7
|
private numChannels;
|
|
7
8
|
private noiseCancellation?;
|
|
9
|
+
private frameProcessor?;
|
|
8
10
|
private publication;
|
|
9
11
|
private participantIdentity;
|
|
10
12
|
private logger;
|
|
@@ -12,12 +14,13 @@ export declare class ParticipantAudioInputStream extends AudioInput {
|
|
|
12
14
|
room: Room;
|
|
13
15
|
sampleRate: number;
|
|
14
16
|
numChannels: number;
|
|
15
|
-
noiseCancellation?: NoiseCancellationOptions
|
|
17
|
+
noiseCancellation?: NoiseCancellationOptions | FrameProcessor<AudioFrame>;
|
|
16
18
|
});
|
|
17
19
|
setParticipant(participant: RemoteParticipant | string | null): void;
|
|
18
20
|
private onTrackUnpublished;
|
|
19
21
|
private closeStream;
|
|
20
22
|
private onTrackSubscribed;
|
|
23
|
+
private onTokenRefreshed;
|
|
21
24
|
private createStream;
|
|
22
25
|
close(): Promise<void>;
|
|
23
26
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"_input.d.ts","sourceRoot":"","sources":["../../../src/voice/room_io/_input.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"_input.d.ts","sourceRoot":"","sources":["../../../src/voice/room_io/_input.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,UAAU,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACpE,OAAO,EAEL,KAAK,wBAAwB,EAC7B,iBAAiB,EAGjB,KAAK,IAAI,EAGV,MAAM,mBAAmB,CAAC;AAI3B,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAEtC,qBAAa,2BAA4B,SAAQ,UAAU;IACzD,OAAO,CAAC,IAAI,CAAO;IACnB,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,iBAAiB,CAAC,CAA2B;IACrD,OAAO,CAAC,cAAc,CAAC,CAA6B;IACpD,OAAO,CAAC,WAAW,CAAuC;IAC1D,OAAO,CAAC,mBAAmB,CAAuB;IAClD,OAAO,CAAC,MAAM,CAAS;gBACX,EACV,IAAI,EACJ,UAAU,EACV,WAAW,EACX,iBAAiB,GAClB,EAAE;QACD,IAAI,EAAE,IAAI,CAAC;QACX,UAAU,EAAE,MAAM,CAAC;QACnB,WAAW,EAAE,MAAM,CAAC;QACpB,iBAAiB,CAAC,EAAE,wBAAwB,GAAG,cAAc,CAAC,UAAU,CAAC,CAAC;KAC3E;IAgBD,cAAc,CAAC,WAAW,EAAE,iBAAiB,GAAG,MAAM,GAAG,IAAI;IA2C7D,OAAO,CAAC,kBAAkB,CAqBxB;IAEF,OAAO,CAAC,WAAW;IAUnB,OAAO,CAAC,iBAAiB,CA+BvB;IAEF,OAAO,CAAC,gBAAgB,CAOtB;IAEF,OAAO,CAAC,YAAY;IASd,KAAK;CAQZ"}
|