@livekit/agents 1.0.45 → 1.0.47
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 +14 -20
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +14 -20
- package/dist/cli.js.map +1 -1
- package/dist/ipc/job_proc_lazy_main.cjs +14 -5
- package/dist/ipc/job_proc_lazy_main.cjs.map +1 -1
- package/dist/ipc/job_proc_lazy_main.js +14 -5
- package/dist/ipc/job_proc_lazy_main.js.map +1 -1
- package/dist/llm/chat_context.cjs +19 -0
- package/dist/llm/chat_context.cjs.map +1 -1
- package/dist/llm/chat_context.d.cts +4 -0
- package/dist/llm/chat_context.d.ts +4 -0
- package/dist/llm/chat_context.d.ts.map +1 -1
- package/dist/llm/chat_context.js +19 -0
- package/dist/llm/chat_context.js.map +1 -1
- package/dist/llm/provider_format/index.cjs +2 -0
- package/dist/llm/provider_format/index.cjs.map +1 -1
- package/dist/llm/provider_format/index.d.cts +1 -1
- package/dist/llm/provider_format/index.d.ts +1 -1
- package/dist/llm/provider_format/index.d.ts.map +1 -1
- package/dist/llm/provider_format/index.js +6 -1
- package/dist/llm/provider_format/index.js.map +1 -1
- package/dist/llm/provider_format/openai.cjs +82 -2
- package/dist/llm/provider_format/openai.cjs.map +1 -1
- package/dist/llm/provider_format/openai.d.cts +1 -0
- package/dist/llm/provider_format/openai.d.ts +1 -0
- package/dist/llm/provider_format/openai.d.ts.map +1 -1
- package/dist/llm/provider_format/openai.js +80 -1
- package/dist/llm/provider_format/openai.js.map +1 -1
- package/dist/llm/provider_format/openai.test.cjs +326 -0
- package/dist/llm/provider_format/openai.test.cjs.map +1 -1
- package/dist/llm/provider_format/openai.test.js +327 -1
- package/dist/llm/provider_format/openai.test.js.map +1 -1
- package/dist/llm/provider_format/utils.cjs +4 -3
- package/dist/llm/provider_format/utils.cjs.map +1 -1
- package/dist/llm/provider_format/utils.d.ts.map +1 -1
- package/dist/llm/provider_format/utils.js +4 -3
- package/dist/llm/provider_format/utils.js.map +1 -1
- package/dist/llm/realtime.cjs.map +1 -1
- package/dist/llm/realtime.d.cts +1 -0
- package/dist/llm/realtime.d.ts +1 -0
- package/dist/llm/realtime.d.ts.map +1 -1
- package/dist/llm/realtime.js.map +1 -1
- package/dist/log.cjs +5 -2
- package/dist/log.cjs.map +1 -1
- package/dist/log.d.ts.map +1 -1
- package/dist/log.js +5 -2
- package/dist/log.js.map +1 -1
- package/dist/stream/deferred_stream.cjs +15 -6
- package/dist/stream/deferred_stream.cjs.map +1 -1
- package/dist/stream/deferred_stream.d.ts.map +1 -1
- package/dist/stream/deferred_stream.js +15 -6
- package/dist/stream/deferred_stream.js.map +1 -1
- package/dist/stream/index.cjs +3 -0
- package/dist/stream/index.cjs.map +1 -1
- package/dist/stream/index.d.cts +1 -0
- package/dist/stream/index.d.ts +1 -0
- package/dist/stream/index.d.ts.map +1 -1
- package/dist/stream/index.js +2 -0
- package/dist/stream/index.js.map +1 -1
- package/dist/stream/multi_input_stream.cjs +139 -0
- package/dist/stream/multi_input_stream.cjs.map +1 -0
- package/dist/stream/multi_input_stream.d.cts +55 -0
- package/dist/stream/multi_input_stream.d.ts +55 -0
- package/dist/stream/multi_input_stream.d.ts.map +1 -0
- package/dist/stream/multi_input_stream.js +115 -0
- package/dist/stream/multi_input_stream.js.map +1 -0
- package/dist/stream/multi_input_stream.test.cjs +340 -0
- package/dist/stream/multi_input_stream.test.cjs.map +1 -0
- package/dist/stream/multi_input_stream.test.js +339 -0
- package/dist/stream/multi_input_stream.test.js.map +1 -0
- package/dist/telemetry/trace_types.cjs +42 -0
- package/dist/telemetry/trace_types.cjs.map +1 -1
- package/dist/telemetry/trace_types.d.cts +14 -0
- package/dist/telemetry/trace_types.d.ts +14 -0
- package/dist/telemetry/trace_types.d.ts.map +1 -1
- package/dist/telemetry/trace_types.js +28 -0
- package/dist/telemetry/trace_types.js.map +1 -1
- package/dist/utils.cjs +44 -2
- package/dist/utils.cjs.map +1 -1
- package/dist/utils.d.cts +8 -0
- package/dist/utils.d.ts +8 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +44 -2
- package/dist/utils.js.map +1 -1
- package/dist/utils.test.cjs +71 -0
- package/dist/utils.test.cjs.map +1 -1
- package/dist/utils.test.js +71 -0
- package/dist/utils.test.js.map +1 -1
- package/dist/version.cjs +1 -1
- package/dist/version.cjs.map +1 -1
- package/dist/version.d.cts +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/dist/voice/agent.cjs +144 -12
- package/dist/voice/agent.cjs.map +1 -1
- package/dist/voice/agent.d.cts +29 -4
- package/dist/voice/agent.d.ts +29 -4
- package/dist/voice/agent.d.ts.map +1 -1
- package/dist/voice/agent.js +140 -11
- package/dist/voice/agent.js.map +1 -1
- package/dist/voice/agent.test.cjs +120 -0
- package/dist/voice/agent.test.cjs.map +1 -1
- package/dist/voice/agent.test.js +122 -2
- package/dist/voice/agent.test.js.map +1 -1
- package/dist/voice/agent_activity.cjs +402 -292
- package/dist/voice/agent_activity.cjs.map +1 -1
- package/dist/voice/agent_activity.d.cts +35 -7
- package/dist/voice/agent_activity.d.ts +35 -7
- package/dist/voice/agent_activity.d.ts.map +1 -1
- package/dist/voice/agent_activity.js +402 -287
- package/dist/voice/agent_activity.js.map +1 -1
- package/dist/voice/agent_session.cjs +156 -44
- package/dist/voice/agent_session.cjs.map +1 -1
- package/dist/voice/agent_session.d.cts +22 -9
- package/dist/voice/agent_session.d.ts +22 -9
- package/dist/voice/agent_session.d.ts.map +1 -1
- package/dist/voice/agent_session.js +156 -44
- package/dist/voice/agent_session.js.map +1 -1
- package/dist/voice/audio_recognition.cjs +89 -36
- package/dist/voice/audio_recognition.cjs.map +1 -1
- package/dist/voice/audio_recognition.d.cts +22 -1
- package/dist/voice/audio_recognition.d.ts +22 -1
- package/dist/voice/audio_recognition.d.ts.map +1 -1
- package/dist/voice/audio_recognition.js +93 -36
- package/dist/voice/audio_recognition.js.map +1 -1
- package/dist/voice/audio_recognition_span.test.cjs +233 -0
- package/dist/voice/audio_recognition_span.test.cjs.map +1 -0
- package/dist/voice/audio_recognition_span.test.js +232 -0
- package/dist/voice/audio_recognition_span.test.js.map +1 -0
- package/dist/voice/generation.cjs +39 -19
- package/dist/voice/generation.cjs.map +1 -1
- package/dist/voice/generation.d.ts.map +1 -1
- package/dist/voice/generation.js +44 -20
- package/dist/voice/generation.js.map +1 -1
- package/dist/voice/index.cjs +2 -0
- package/dist/voice/index.cjs.map +1 -1
- package/dist/voice/index.d.cts +1 -1
- package/dist/voice/index.d.ts +1 -1
- package/dist/voice/index.d.ts.map +1 -1
- package/dist/voice/index.js +2 -1
- package/dist/voice/index.js.map +1 -1
- package/dist/voice/io.cjs +6 -3
- package/dist/voice/io.cjs.map +1 -1
- package/dist/voice/io.d.cts +3 -2
- package/dist/voice/io.d.ts +3 -2
- package/dist/voice/io.d.ts.map +1 -1
- package/dist/voice/io.js +6 -3
- package/dist/voice/io.js.map +1 -1
- package/dist/voice/recorder_io/recorder_io.cjs +3 -1
- package/dist/voice/recorder_io/recorder_io.cjs.map +1 -1
- package/dist/voice/recorder_io/recorder_io.d.ts.map +1 -1
- package/dist/voice/recorder_io/recorder_io.js +3 -1
- package/dist/voice/recorder_io/recorder_io.js.map +1 -1
- package/dist/voice/room_io/_input.cjs +17 -17
- package/dist/voice/room_io/_input.cjs.map +1 -1
- package/dist/voice/room_io/_input.d.cts +2 -2
- package/dist/voice/room_io/_input.d.ts +2 -2
- package/dist/voice/room_io/_input.d.ts.map +1 -1
- package/dist/voice/room_io/_input.js +7 -6
- package/dist/voice/room_io/_input.js.map +1 -1
- package/dist/voice/room_io/room_io.cjs +9 -0
- package/dist/voice/room_io/room_io.cjs.map +1 -1
- package/dist/voice/room_io/room_io.d.cts +3 -1
- package/dist/voice/room_io/room_io.d.ts +3 -1
- package/dist/voice/room_io/room_io.d.ts.map +1 -1
- package/dist/voice/room_io/room_io.js +9 -0
- package/dist/voice/room_io/room_io.js.map +1 -1
- package/dist/voice/speech_handle.cjs +7 -1
- package/dist/voice/speech_handle.cjs.map +1 -1
- package/dist/voice/speech_handle.d.cts +2 -0
- package/dist/voice/speech_handle.d.ts +2 -0
- package/dist/voice/speech_handle.d.ts.map +1 -1
- package/dist/voice/speech_handle.js +8 -2
- package/dist/voice/speech_handle.js.map +1 -1
- package/dist/voice/testing/run_result.cjs +66 -15
- package/dist/voice/testing/run_result.cjs.map +1 -1
- package/dist/voice/testing/run_result.d.cts +14 -3
- package/dist/voice/testing/run_result.d.ts +14 -3
- package/dist/voice/testing/run_result.d.ts.map +1 -1
- package/dist/voice/testing/run_result.js +66 -15
- package/dist/voice/testing/run_result.js.map +1 -1
- package/dist/voice/utils.cjs +47 -0
- package/dist/voice/utils.cjs.map +1 -0
- package/dist/voice/utils.d.cts +4 -0
- package/dist/voice/utils.d.ts +4 -0
- package/dist/voice/utils.d.ts.map +1 -0
- package/dist/voice/utils.js +23 -0
- package/dist/voice/utils.js.map +1 -0
- package/package.json +1 -1
- package/src/cli.ts +20 -33
- package/src/ipc/job_proc_lazy_main.ts +16 -5
- package/src/llm/chat_context.ts +35 -0
- package/src/llm/provider_format/index.ts +7 -2
- package/src/llm/provider_format/openai.test.ts +385 -1
- package/src/llm/provider_format/openai.ts +103 -0
- package/src/llm/provider_format/utils.ts +6 -4
- package/src/llm/realtime.ts +1 -0
- package/src/log.ts +5 -2
- package/src/stream/deferred_stream.ts +17 -6
- package/src/stream/index.ts +1 -0
- package/src/stream/multi_input_stream.test.ts +540 -0
- package/src/stream/multi_input_stream.ts +172 -0
- package/src/telemetry/trace_types.ts +18 -0
- package/src/utils.test.ts +87 -0
- package/src/utils.ts +52 -2
- package/src/version.ts +1 -1
- package/src/voice/agent.test.ts +140 -2
- package/src/voice/agent.ts +189 -10
- package/src/voice/agent_activity.ts +449 -286
- package/src/voice/agent_session.ts +195 -51
- package/src/voice/audio_recognition.ts +118 -38
- package/src/voice/audio_recognition_span.test.ts +261 -0
- package/src/voice/generation.ts +52 -23
- package/src/voice/index.ts +1 -1
- package/src/voice/io.ts +7 -4
- package/src/voice/recorder_io/recorder_io.ts +2 -1
- package/src/voice/room_io/_input.ts +11 -7
- package/src/voice/room_io/room_io.ts +12 -0
- package/src/voice/speech_handle.ts +9 -2
- package/src/voice/testing/run_result.ts +81 -23
- package/src/voice/utils.ts +29 -0
|
@@ -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(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":"AAGA,OAAO,qBAAqB;AAC5B,SAAS,aAAa;AACtB,SAAS,YAAY,sBAAsB;AAC3C,OAAO,YAAY;AACnB,OAAO,QAAQ;AACf,OAAO,UAAU;AACjB,SAAS,mBAAmB;AAE5B,SAAS,uBAAuB;AAChC,SAAS,WAAW;AACpB,SAAS,kCAAkC;AAC3C,SAA6B,2BAA2B;AACxD,SAAS,QAAQ,MAAM,eAAe,aAAa;AAEnD,SAAS,YAAY,mBAA+C;AAEpE,OAAO,cAAc,gBAAgB,IAAI;AAEzC,MAAM,oBAAoB;AAC1B,MAAM,sBAAsB;AAarB,MAAM,WAAW;AAAA,EACd;AAAA,EACA;AAAA,EAEA,SAAsC,oBAAkC;AAAA,EACxE,UAAuC,oBAAkC;AAAA,EAEzE;AAAA,EACA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EAEA,cAA4B,IAAI,OAAO;AAAA,EACvC,OAAc,IAAI,MAAM;AAAA,EACxB,UAAmB;AAAA;AAAA,EAGnB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEA,SAAS,IAAI;AAAA,EAErB,YAAY,MAAuB;AACjC,UAAM,EAAE,cAAc,aAAa,oBAAoB,IAAI;AAE3D,SAAK,UAAU;AACf,SAAK,aAAa;AAAA,EACpB;AAAA,EAEA,MAAM,MAAM,YAAmC;AAC7C,UAAM,SAAS,MAAM,KAAK,KAAK,KAAK;AAEpC,QAAI;AACF,UAAI,KAAK,QAAS;AAElB,UAAI,CAAC,KAAK,YAAY,CAAC,KAAK,WAAW;AACrC,cAAM,IAAI;AAAA,UACR;AAAA,QACF;AAAA,MACF;AAEA,WAAK,cAAc;AACnB,WAAK,UAAU;AACf,WAAK,cAAc,IAAI,OAAO;AAG9B,YAAM,MAAM,KAAK,QAAQ,UAAU;AACnC,UAAI,CAAC,GAAG,WAAW,GAAG,GAAG;AACvB,WAAG,UAAU,KAAK,EAAE,WAAW,KAAK,CAAC;AAAA,MACvC;AAEA,WAAK,cAAc,KAAK,KAAK,CAAC,EAAE,OAAO,MAAM,KAAK,QAAQ,MAAM,CAAC;AACjE,WAAK,aAAa,KAAK,KAAK,MAAM,KAAK,OAAO,GAAG,QAAW,yBAAyB;AAAA,IACvF,UAAE;AACA,aAAO;AAAA,IACT;AAAA,EACF;AAAA,EAEA,MAAM,QAAuB;AAC3B,UAAM,SAAS,MAAM,KAAK,KAAK,KAAK;AAEpC,QAAI;AACF,UAAI,CAAC,KAAK,QAAS;AAEnB,YAAM,KAAK,OAAO,MAAM;AACxB,YAAM,KAAK,QAAQ,MAAM;AACzB,YAAM,KAAK,YAAY;AACvB,YAAM,cAAc,CAAC,KAAK,aAAc,KAAK,UAAW,CAAC;AAEzD,WAAK,UAAU;AAAA,IACjB,UAAE;AACA,aAAO;AAAA,IACT;AAAA,EACF;AAAA,EAEA,YAAY,YAA4C;AACtD,SAAK,WAAW,IAAI,mBAAmB,MAAM,UAAU;AACvD,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,aAAa,aAA+C;AAC1D,SAAK,YAAY,IAAI,oBAAoB,MAAM,aAAa,CAAC,QAAQ,KAAK,QAAQ,GAAG,CAAC;AACtF,WAAO,KAAK;AAAA,EACd;AAAA,EAEQ,QAAQ,KAAyB;AA5H3C;AA6HI,UAAM,WAAW,KAAK,SAAU,SAAQ,UAAK,cAAL,mBAAgB,kBAAkB;AAC1E,SAAK,OAAO,MAAM,QAAQ;AAC1B,SAAK,QAAQ,MAAM,GAAG;AAAA,EACxB;AAAA,EAEA,IAAI,YAAqB;AACvB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,IAAI,aAAiC;AACnC,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,IAAI,qBAAyC;AA1I/C;AA2II,UAAM,OAAM,UAAK,aAAL,mBAAe;AAC3B,UAAM,QAAO,UAAK,cAAL,mBAAgB;AAE7B,QAAI,QAAQ,QAAW;AACrB,aAAO;AAAA,IACT;AAEA,QAAI,SAAS,QAAW;AACtB,aAAO;AAAA,IACT;AAEA,WAAO,KAAK,IAAI,KAAK,IAAI;AAAA,EAC3B;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,QAAQ,QAAoC;AACxD,WAAO,CAAC,OAAO,SAAS;AACtB,UAAI;AACF,cAAM,MAAM,mBAAmB,EAAE,OAAO,CAAC;AAAA,MAC3C,QAAQ;AAEN;AAAA,MACF;AAEA,UAAI,KAAK,UAAW,gBAAgB;AAElC;AAAA,MACF;AAGA,YAAM,WAAW,KAAK,SAAU,QAAQ,KAAK,UAAW,kBAAkB;AAC1E,WAAK,OACF,MAAM,QAAQ,EACd,MAAM,CAAC,QAAQ,KAAK,OAAO,MAAM,EAAE,IAAI,GAAG,uCAAuC,CAAC;AACrF,WAAK,QACF,MAAM,CAAC,CAAC,EACR,MAAM,CAAC,QAAQ,KAAK,OAAO,MAAM,EAAE,IAAI,GAAG,wCAAwC,CAAC;AAAA,IACxF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKQ,cAAoB;AAC1B,QAAI,KAAK,UAAW;AAEpB,SAAK,YAAY,IAAI,YAAY;AAEjC,SAAK,gBAAgB,IAAI,QAAc,CAAC,SAAS,WAAW;AAC1D,aAAO,KAAK,SAAU,EACnB,YAAY,OAAO,EACnB,aAAa,CAAC,OAAO,KAAK,UAAU,IAAI,OAAO,CAAC,EAChD,WAAW,SAAS,EACpB,cAAc,CAAC,EACf,eAAe,KAAK,UAAU,EAC9B,OAAO,KAAK,EACZ,OAAO,KAAK,WAAY,EACxB,GAAG,OAAO,MAAM;AACf,aAAK,OAAO,MAAM,0BAA0B;AAC5C,gBAAQ;AAAA,MACV,CAAC,EACA,GAAG,SAAS,CAAC,QAAQ;AA1M9B;AA4MU,cACE,SAAI,YAAJ,mBAAa,SAAS,8BACtB,SAAI,YAAJ,mBAAa,SAAS,2BACtB,SAAI,YAAJ,mBAAa,SAAS,iBACtB,SAAI,YAAJ,mBAAa,SAAS,YACtB;AACA,kBAAQ;AAAA,QACV,OAAO;AACL,eAAK,OAAO,MAAM,EAAE,IAAI,GAAG,uBAAuB;AAClD,iBAAO,GAAG;AAAA,QACZ;AAAA,MACF,CAAC,EACA,IAAI;AAAA,IACT,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKQ,eAAe,MAGrB;AACA,UAAM,YAAY,IAAM;AACxB,UAAM,EAAE,QAAQ,QAAQ,MAAM,IAAI;AAClC,QAAI,EAAE,UAAU,IAAI;AAEpB,QAAI,OAAO,WAAW,KAAK,CAAC,OAAO;AACjC,aAAO,EAAE,SAAS,IAAI,aAAa,CAAC,GAAG,UAAU;AAAA,IACnD;AAEA,QAAI,CAAC,aAAa,OAAO,SAAS,GAAG;AACnC,YAAM,aAAa,OAAO,CAAC;AAC3B,kBAAY,IAAI,eAAe,WAAW,YAAY,KAAK,YAAY,WAAW,QAAQ;AAAA,IAC5F;AAEA,UAAM,kBAAgC,CAAC;AACvC,eAAW,SAAS,QAAQ;AAC1B,UAAI,WAAW;AACb,wBAAgB,KAAK,GAAG,UAAU,KAAK,KAAK,CAAC;AAAA,MAC/C;AAAA,IACF;AAEA,QAAI,SAAS,WAAW;AACtB,sBAAgB,KAAK,GAAG,UAAU,MAAM,CAAC;AAAA,IAC3C;AAEA,UAAM,eAAe,gBAAgB,OAAO,CAAC,KAAK,UAAU,MAAM,MAAM,mBAAmB,CAAC;AAC5F,UAAM,UAAU,IAAI,aAAa,YAAY;AAE7C,QAAI,MAAM;AACV,eAAW,SAAS,iBAAiB;AACnC,YAAM,OAAO,MAAM;AACnB,YAAM,cAAc,MAAM;AAC1B,eAAS,IAAI,GAAG,IAAI,MAAM,mBAAmB,KAAK;AAChD,YAAI,MAAM;AACV,iBAAS,KAAK,GAAG,KAAK,aAAa,MAAM;AACvC,iBAAO,KAAK,IAAI,cAAc,EAAE;AAAA,QAClC;AACA,gBAAQ,KAAK,IAAK,MAAM,cAAe;AAAA,MACzC;AAAA,IACF;AAEA,WAAO,EAAE,SAAS,UAAU;AAAA,EAC9B;AAAA;AAAA;AAAA;AAAA,EAKQ,SAAS,aAA2B,cAAkC;AAC5E,QAAI,CAAC,KAAK,WAAW;AACnB,WAAK,YAAY;AAAA,IACnB;AAGA,QAAI,YAAY,WAAW,aAAa,QAAQ;AAC9C,YAAM,OAAO,KAAK,IAAI,YAAY,SAAS,aAAa,MAAM;AAC9D,UAAI,YAAY,SAAS,aAAa,QAAQ;AAC5C,aAAK,OAAO;AAAA,UACV,uBAAuB,IAAI;AAAA,QAC7B;AACA,cAAM,SAAS,IAAI,aAAa,aAAa,MAAM;AACnD,eAAO,IAAI,aAAa,IAAI;AAC5B,sBAAc;AAAA,MAChB,OAAO;AACL,cAAM,SAAS,IAAI,aAAa,YAAY,MAAM;AAClD,eAAO,IAAI,cAAc,IAAI;AAC7B,uBAAe;AAAA,MACjB;AAAA,IACF;AAEA,UAAM,SAAS,KAAK,IAAI,YAAY,QAAQ,aAAa,MAAM;AAC/D,QAAI,UAAU,EAAG;AAGjB,UAAM,aAAa,IAAI,WAAW,SAAS,CAAC;AAC5C,aAAS,IAAI,GAAG,IAAI,QAAQ,KAAK;AAC/B,iBAAW,IAAI,CAAC,IAAI,KAAK;AAAA,QACvB;AAAA,QACA,KAAK,IAAI,OAAO,KAAK,OAAO,YAAY,CAAC,KAAK,KAAK,KAAK,CAAC;AAAA,MAC3D;AACA,iBAAW,IAAI,IAAI,CAAC,IAAI,KAAK;AAAA,QAC3B;AAAA,QACA,KAAK,IAAI,OAAO,KAAK,OAAO,aAAa,CAAC,KAAK,KAAK,KAAK,CAAC;AAAA,MAC5D;AAAA,IACF;AAEA,SAAK,UAAW,MAAM,OAAO,KAAK,WAAW,MAAM,CAAC;AAAA,EACtD;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,SAAwB;AACpC,QAAI,CAAC,KAAK,YAAa;AAEvB,UAAM,WAAW,KAAK,OAAO,OAAO,EAAE,UAAU;AAChD,UAAM,YAAY,KAAK,QAAQ,OAAO,EAAE,UAAU;AAElD,QAAI;AACF,aAAO,MAAM;AACX,cAAM,CAAC,UAAU,SAAS,IAAI,MAAM,QAAQ,IAAI,CAAC,SAAS,KAAK,GAAG,UAAU,KAAK,CAAC,CAAC;AAEnF,YAAI,SAAS,QAAQ,UAAU,MAAM;AACnC;AAAA,QACF;AAEA,cAAM,WAAW,SAAS;AAC1B,cAAM,YAAY,UAAU;AAE5B,cAAM,UAAU,KAAK,eAAe,EAAE,QAAQ,UAAU,WAAW,KAAK,YAAY,CAAC;AACrF,aAAK,cAAc,QAAQ;AAE3B,cAAM,WAAW,KAAK,eAAe;AAAA,UACnC,QAAQ;AAAA,UACR,WAAW,KAAK;AAAA,UAChB,OAAO,UAAU,SAAS;AAAA,QAC5B,CAAC;AACD,aAAK,eAAe,SAAS;AAG7B,aAAK,SAAS,QAAQ,SAAS,SAAS,OAAO;AAAA,MACjD;AAGA,UAAI,KAAK,WAAW;AAClB,aAAK,UAAU,IAAI;AACnB,cAAM,KAAK;AAAA,MACb;AAAA,IACF,SAAS,KAAK;AACZ,WAAK,OAAO,MAAM,EAAE,IAAI,GAAG,sBAAsB;AAAA,IACnD,UAAE;AACA,eAAS,YAAY;AACrB,gBAAU,YAAY;AAEtB,UAAI,CAAC,KAAK,YAAY,MAAM;AAC1B,aAAK,YAAY,QAAQ;AAAA,MAC3B;AAAA,IACF;AAAA,EACF;AACF;AAEA,MAAM,2BAA2B,WAAW;AAAA,EAClC;AAAA,EACA;AAAA,EACA,YAA0B,CAAC;AAAA,EAC3B;AAAA,EACA,UAAmB;AAAA,EACnB,SAAS,IAAI;AAAA,EAErB,YAAY,YAAwB,QAAoB;AACtD,UAAM;AACN,SAAK,aAAa;AAClB,SAAK,SAAS;AAGd,SAAK,eAAe,UAAU,KAAK,yBAAyB,CAAC;AAAA,EAC/D;AAAA;AAAA;AAAA;AAAA,EAKA,IAAI,kBAAsC;AACxC,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,QAAQ,UAAiC;AACvC,QAAI,SAAS,KAAK;AAClB,SAAK,YAAY,CAAC;AAElB,QACE,aAAa,UACb,KAAK,qBAAqB,UAC1B,KAAK,mBAAmB,YACxB,CAAC,KAAK,WACN,OAAO,SAAS,GAChB;AACA,YAAM,UAAU,KAAK,mBAAmB;AACxC,WAAK,OAAO;AAAA,QACV;AAAA,UACE,qBAAqB;AAAA,UACrB,kBAAkB,KAAK;AAAA,QACzB;AAAA,QACA;AAAA,MACF;AACA,WAAK,UAAU;AACf,YAAM,aAAa,OAAO,CAAC;AAC3B,eAAS;AAAA,QACP,mBAAmB,UAAU,KAAM,WAAW,YAAY,WAAW,QAAQ;AAAA,QAC7E,GAAG;AAAA,MACL;AAAA,IACF,WACE,aAAa,UACb,KAAK,qBAAqB,UAC1B,CAAC,KAAK,WACN,OAAO,WAAW,GAClB;AAGA,WAAK,OAAO;AAAA,QACV;AAAA,MACF;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,2BAAuD;AAC7D,UAAM,eAAe,KAAK,OAAO;AACjC,UAAM,SAAS,aAAa,UAAU;AAEtC,UAAM,YAAY,IAAI,gBAAwC;AAAA,MAC5D,WAAW,CAAC,OAAO,eAAe;AAEhC,YAAI,KAAK,WAAW,WAAW;AAC7B,cAAI,KAAK,qBAAqB,QAAW;AACvC,iBAAK,mBAAmB,KAAK,IAAI;AAAA,UACnC;AACA,eAAK,UAAU,KAAK,KAAK;AAAA,QAC3B;AAEA,mBAAW,QAAQ,KAAK;AAAA,MAC1B;AAAA,IACF,CAAC;AAED,UAAM,OAAO,YAAY;AACvB,YAAM,SAAS,UAAU,SAAS,UAAU;AAC5C,UAAI;AAEJ,UAAI;AACF,eAAO,MAAM;AACX,gBAAM,EAAE,MAAM,MAAM,IAAI,MAAM,OAAO,KAAK;AAC1C,cAAI,KAAM;AACV,gBAAM,OAAO,MAAM,KAAK;AAAA,QAC1B;AAAA,MACF,SAAS,GAAG;AACV,YAAI,2BAA2B,CAAC,EAAG;AACnC,sBAAc;AAAA,MAChB,UAAE;AACA,YAAI,aAAa;AACf,iBAAO,MAAM,WAAW;AACxB;AAAA,QACF;AAEA,eAAO,YAAY;AAEnB,YAAI;AACF,gBAAM,UAAU,SAAS,MAAM;AAAA,QACjC,QAAQ;AAAA,QAER;AAAA,MACF;AAAA,IACF;AAEA,SAAK;AAEL,WAAO,UAAU;AAAA,EACnB;AAAA,EAEA,aAAmB;AACjB,SAAK,OAAO,WAAW;AAAA,EACzB;AAAA,EAEA,aAAmB;AACjB,SAAK,OAAO,WAAW;AAAA,EACzB;AACF;AAEA,MAAM,4BAA4B,YAAY;AAAA,EACpC;AAAA,EACA;AAAA,EACA,YAA0B,CAAC;AAAA,EAC3B;AAAA,EACA,UAAU,IAAI;AAAA,EAEtB;AAAA,EACQ;AAAA;AAAA,EAGA;AAAA,EACA,iBAA0C,CAAC;AAAA;AAAA,EAEnD,YACE,YACA,aACA,SACA;AACA,UAAM,YAAY,YAAY,aAAa,EAAE,OAAO,KAAK,CAAC;AAC1D,SAAK,aAAa;AAClB,SAAK,UAAU;AAAA,EACjB;AAAA,EAEA,IAAI,kBAAsC;AACxC,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,IAAI,iBAA0B;AAC5B,WAAO,KAAK,UAAU,SAAS;AAAA,EACjC;AAAA,EAEA,QAAc;AACZ,QAAI,KAAK,sBAAsB,UAAa,KAAK,WAAW,WAAW;AACrE,WAAK,oBAAoB,KAAK,IAAI;AAAA,IACpC;AAEA,QAAI,KAAK,aAAa;AACpB,WAAK,YAAY,MAAM;AAAA,IACzB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,SAAe;AACb,QAAI,KAAK,sBAAsB,UAAa,KAAK,WAAW,WAAW;AACrE,WAAK,eAAe,KAAK,CAAC,KAAK,mBAAmB,KAAK,IAAI,CAAC,CAAC;AAC7D,WAAK,oBAAoB;AAAA,IAC3B;AAEA,QAAI,KAAK,aAAa;AACpB,WAAK,YAAY,OAAO;AAAA,IAC1B;AAAA,EACF;AAAA,EAEQ,kBAAwB;AAC9B,SAAK,oBAAoB;AACzB,SAAK,iBAAiB,CAAC;AAAA,EACzB;AAAA,EAEA,mBAAmB,SAAsC;AACvD,UAAM,aAAa,KAAK,qBAAqB,KAAK,IAAI;AACtD,UAAM,0BAA0B,KAAK,IAAI,GAAG,KAAK,IAAI,IAAI,UAAU;AAGnE,QAAI,mBAAmB,QAAQ,mBAAmB;AAElD,QAAI,KAAK,yBAAyB,QAAW;AAC3C,WAAK,QAAQ;AAAA,QACX;AAAA,UACE;AAAA,UACA;AAAA,UACA,aAAa,QAAQ;AAAA,QACvB;AAAA,QACA;AAAA,MACF;AACA,yBAAmB;AAAA,IACrB;AAGA,uBAAmB,KAAK;AAAA,MACtB;AAAA,MACA,KAAK,IAAI,cAAc,KAAK,wBAAwB,IAAI,gBAAgB;AAAA,IAC1E;AAGA,UAAM,mBAAmB,EAAE,GAAG,SAAS,kBAAkB,mBAAmB,IAAK,CAAC;AAElF,QAAI,CAAC,KAAK,WAAW,WAAW;AAC9B;AAAA,IACF;AAEA,QAAI,KAAK,sBAAsB,QAAW;AACxC,WAAK,eAAe,KAAK,CAAC,KAAK,mBAAmB,UAAU,CAAC;AAC7D,WAAK,oBAAoB;AAAA,IAC3B;AAEA,QAAI,KAAK,UAAU,WAAW,GAAG;AAC/B,WAAK,gBAAgB;AACrB,WAAK,qBAAqB,KAAK,IAAI;AACnC,WAAK,uBAAuB;AAC5B;AAAA,IACF;AAGA,UAAM,cAAuC,CAAC;AAC9C,QAAI,oBAAoB,aAAa;AAErC,QAAI,KAAK,eAAe,SAAS,GAAG;AAClC,YAAM,qBAAqB,KAAK,eAAe;AAAA,QAC7C,CAAC,KAAK,CAAC,OAAO,GAAG,MAAM,OAAO,MAAM;AAAA,QACpC;AAAA,MACF;AACA,0BAAoB,aAAa,mBAAmB;AAEpD,UAAI,mBAAmB;AACvB,iBAAW,CAAC,YAAY,QAAQ,KAAK,KAAK,gBAAgB;AACxD,YAAI,WAAW,aAAa,oBAAoB;AAChD,cAAM,WAAW,WAAW;AAC5B,mBAAW,KAAK,IAAI,GAAG,KAAK,IAAI,UAAU,gBAAgB,CAAC;AAC3D,oBAAY,KAAK,CAAC,UAAU,QAAQ,CAAC;AACrC,4BAAoB;AAAA,MACtB;AAAA,IACF;AAEA,UAAM,MAAoB,CAAC;AAC3B,QAAI,SAAS;AACb,UAAM,aAAa,KAAK,UAAU,CAAC,EAAG;AACtC,UAAM,cAAc,KAAK,UAAU,CAAC,EAAG;AAEvC,QAAI,WAAW;AACf,QAAI,cAAc;AAElB,eAAW,SAAS,KAAK,WAAW;AAClC,UAAI,eAAe;AACnB,YAAM,gBAAiB,MAAM,oBAAoB,MAAM,aAAc;AAErE,UAAI,gBAAgB,SAAS,kBAAkB;AAC7C,cAAM,CAAC,IAAI,IAAI,WAAW,eAAe,mBAAmB,UAAU,GAAI;AAC1E,uBAAe;AACf,sBAAc;AAAA,MAChB;AAGA,aAAO,WAAW,YAAY,UAAU,YAAY,QAAQ,EAAG,CAAC,KAAK,QAAQ;AAC3E,cAAM,CAAC,EAAE,QAAQ,IAAI,YAAY,QAAQ;AACzC,YAAI,KAAK,mBAAmB,WAAW,KAAM,YAAY,WAAW,CAAC;AACrE;AAAA,MACF;AAGA,YAAM,uBACH,aAAa,oBAAoB,aAAa,aAAc;AAC/D,aACE,WAAW,YAAY,UACvB,YAAY,QAAQ,EAAG,CAAC,IAAI,SAAS,sBACrC;AACA,cAAM,CAAC,UAAU,QAAQ,IAAI,YAAY,QAAQ;AACjD,cAAM,CAAC,MAAM,KAAK,IAAI,WAAW,eAAe,WAAW,UAAU,GAAI;AACzE,YAAI,KAAK,IAAI;AACb,kBAAW,KAAK,oBAAoB,KAAK,aAAc;AACvD,YAAI,KAAK,mBAAmB,WAAW,KAAM,YAAY,WAAW,CAAC;AAErE,uBAAe;AACf;AAAA,MACF;AAEA,UAAI,KAAK,YAAY;AACrB,gBAAW,aAAa,oBAAoB,aAAa,aAAc;AAEvE,UAAI,aAAa;AACf;AAAA,MACF;AAAA,IACF;AAGA,WAAO,WAAW,YAAY,QAAQ;AACpC,YAAM,CAAC,UAAU,QAAQ,IAAI,YAAY,QAAQ;AACjD,UAAI,YAAY,kBAAkB;AAChC,YAAI,KAAK,mBAAmB,WAAW,KAAM,YAAY,WAAW,CAAC;AAAA,MACvE;AACA;AAAA,IACF;AAEA,QAAI,IAAI,SAAS,GAAG;AAClB,UAAI,0BAA0B,GAAG;AAC/B,YAAI,KAAK,mBAAmB,0BAA0B,KAAM,YAAY,WAAW,CAAC;AAAA,MACtF;AACA,WAAK,QAAQ,GAAG;AAAA,IAClB;AAEA,SAAK,YAAY,CAAC;AAClB,SAAK,gBAAgB;AACrB,SAAK,qBAAqB,KAAK,IAAI;AACnC,SAAK,uBAAuB;AAAA,EAC9B;AAAA,EAEA,MAAM,aAAa,OAAkC;AACnD,QAAI,KAAK,aAAa;AACpB,YAAM,KAAK,YAAY,aAAa,KAAK;AAAA,IAC3C;AAEA,UAAM,MAAM,aAAa,KAAK;AAE9B,QAAI,KAAK,WAAW,WAAW;AAC7B,WAAK,UAAU,KAAK,KAAK;AAAA,IAC3B;AAEA,QAAI,KAAK,qBAAqB,QAAW;AACvC,WAAK,mBAAmB,KAAK,IAAI;AAAA,IACnC;AAEA,QAAI,KAAK,yBAAyB,QAAW;AAC3C,WAAK,uBAAuB,KAAK,IAAI;AAAA,IACvC;AAAA,EACF;AAAA,EAEA,QAAc;AACZ,UAAM,MAAM;AAEZ,QAAI,KAAK,aAAa;AACpB,WAAK,YAAY,MAAM;AAAA,IACzB;AAAA,EACF;AAAA,EAEA,cAAoB;AAClB,QAAI,KAAK,aAAa;AACpB,WAAK,YAAY,YAAY;AAAA,IAC/B;AAAA,EACF;AACF;AAKA,SAAS,mBACP,aACA,YACA,aACY;AACZ,QAAM,UAAU,KAAK,MAAM,cAAc,UAAU;AACnD,QAAM,OAAO,IAAI,WAAW,UAAU,WAAW;AACjD,SAAO,IAAI,WAAW,MAAM,YAAY,aAAa,OAAO;AAC9D;AAMA,SAAS,WAAW,OAAmB,UAA4C;AACjF,MAAI,YAAY,GAAG;AACjB,UAAM,aAAa,IAAI,WAAW,IAAI,WAAW,CAAC,GAAG,MAAM,YAAY,MAAM,UAAU,CAAC;AACxF,WAAO,CAAC,YAAY,KAAK;AAAA,EAC3B;AAEA,QAAM,gBAAgB,MAAM,oBAAoB,MAAM;AACtD,MAAI,YAAY,eAAe;AAC7B,UAAM,aAAa,IAAI,WAAW,IAAI,WAAW,CAAC,GAAG,MAAM,YAAY,MAAM,UAAU,CAAC;AACxF,WAAO,CAAC,OAAO,UAAU;AAAA,EAC3B;AAGA,QAAM,gBAAgB,KAAK,MAAM,WAAW,MAAM,UAAU;AAG5D,QAAM,cAAc,MAAM;AAE1B,QAAM,WAAW,MAAM,KAAK,MAAM,GAAG,gBAAgB,WAAW;AAChE,QAAM,YAAY,MAAM,KAAK,MAAM,gBAAgB,WAAW;AAE9D,QAAM,YAAY,IAAI,WAAW,UAAU,MAAM,YAAY,MAAM,UAAU,aAAa;AAE1F,QAAM,aAAa,IAAI;AAAA,IACrB;AAAA,IACA,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM,oBAAoB;AAAA,EAC5B;AAEA,SAAO,CAAC,WAAW,UAAU;AAC/B;","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 await this.inRecord?.close();\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.multiStream.addInputStream(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,7 +22,6 @@ __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");
|
|
26
25
|
var import_log = require("../../log.cjs");
|
|
27
26
|
var import_utils = require("../../utils.cjs");
|
|
28
27
|
var import_io = require("../io.cjs");
|
|
@@ -34,6 +33,7 @@ class ParticipantAudioInputStream extends import_io.AudioInput {
|
|
|
34
33
|
frameProcessor;
|
|
35
34
|
publication = null;
|
|
36
35
|
participantIdentity = null;
|
|
36
|
+
currentInputId = null;
|
|
37
37
|
logger = (0, import_log.log)();
|
|
38
38
|
constructor({
|
|
39
39
|
room,
|
|
@@ -50,13 +50,13 @@ class ParticipantAudioInputStream extends import_io.AudioInput {
|
|
|
50
50
|
} else {
|
|
51
51
|
this.noiseCancellation = noiseCancellation;
|
|
52
52
|
}
|
|
53
|
-
this.room.on(
|
|
54
|
-
this.room.on(
|
|
55
|
-
this.room.on(
|
|
53
|
+
this.room.on(import_rtc_node.RoomEvent.TrackSubscribed, this.onTrackSubscribed);
|
|
54
|
+
this.room.on(import_rtc_node.RoomEvent.TrackUnpublished, this.onTrackUnpublished);
|
|
55
|
+
this.room.on(import_rtc_node.RoomEvent.TokenRefreshed, this.onTokenRefreshed);
|
|
56
56
|
}
|
|
57
57
|
setParticipant(participant) {
|
|
58
58
|
this.logger.debug({ participant }, "setting participant audio input");
|
|
59
|
-
const participantIdentity = participant instanceof
|
|
59
|
+
const participantIdentity = participant instanceof import_rtc_node.RemoteParticipant ? participant.identity : participant;
|
|
60
60
|
if (this.participantIdentity === participantIdentity) {
|
|
61
61
|
return;
|
|
62
62
|
}
|
|
@@ -67,7 +67,7 @@ class ParticipantAudioInputStream extends import_io.AudioInput {
|
|
|
67
67
|
if (!participantIdentity) {
|
|
68
68
|
return;
|
|
69
69
|
}
|
|
70
|
-
const participantValue = participant instanceof
|
|
70
|
+
const participantValue = participant instanceof import_rtc_node.RemoteParticipant ? participant : this.room.remoteParticipants.get(participantIdentity);
|
|
71
71
|
const trackPublicationsArray = Array.from((participantValue == null ? void 0 : participantValue.trackPublications.values()) ?? []);
|
|
72
72
|
this.logger.info(
|
|
73
73
|
{
|
|
@@ -79,7 +79,7 @@ class ParticipantAudioInputStream extends import_io.AudioInput {
|
|
|
79
79
|
);
|
|
80
80
|
if (participantValue) {
|
|
81
81
|
for (const publication of participantValue.trackPublications.values()) {
|
|
82
|
-
if (publication.track && publication.source ===
|
|
82
|
+
if (publication.track && publication.source === import_rtc_node.TrackSource.SOURCE_MICROPHONE) {
|
|
83
83
|
this.onTrackSubscribed(publication.track, publication, participantValue);
|
|
84
84
|
break;
|
|
85
85
|
}
|
|
@@ -99,20 +99,21 @@ class ParticipantAudioInputStream extends import_io.AudioInput {
|
|
|
99
99
|
}
|
|
100
100
|
};
|
|
101
101
|
closeStream() {
|
|
102
|
-
if (this.
|
|
103
|
-
this.
|
|
102
|
+
if (this.currentInputId) {
|
|
103
|
+
void this.multiStream.removeInputStream(this.currentInputId);
|
|
104
|
+
this.currentInputId = null;
|
|
104
105
|
}
|
|
105
106
|
this.publication = null;
|
|
106
107
|
}
|
|
107
108
|
onTrackSubscribed = (track, publication, participant) => {
|
|
108
109
|
var _a, _b;
|
|
109
110
|
this.logger.debug({ participant: participant.identity }, "onTrackSubscribed in _input");
|
|
110
|
-
if (this.participantIdentity !== participant.identity || publication.source !==
|
|
111
|
+
if (this.participantIdentity !== participant.identity || publication.source !== import_rtc_node.TrackSource.SOURCE_MICROPHONE || this.publication && this.publication.sid === publication.sid) {
|
|
111
112
|
return false;
|
|
112
113
|
}
|
|
113
114
|
this.closeStream();
|
|
114
115
|
this.publication = publication;
|
|
115
|
-
this.
|
|
116
|
+
this.currentInputId = this.multiStream.addInputStream(
|
|
116
117
|
(0, import_utils.resampleStream)({
|
|
117
118
|
stream: this.createStream(track),
|
|
118
119
|
outputRate: this.sampleRate
|
|
@@ -139,7 +140,7 @@ class ParticipantAudioInputStream extends import_io.AudioInput {
|
|
|
139
140
|
}
|
|
140
141
|
};
|
|
141
142
|
createStream(track) {
|
|
142
|
-
return new
|
|
143
|
+
return new import_rtc_node.AudioStream(track, {
|
|
143
144
|
sampleRate: this.sampleRate,
|
|
144
145
|
numChannels: this.numChannels,
|
|
145
146
|
noiseCancellation: this.frameProcessor || this.noiseCancellation
|
|
@@ -148,14 +149,13 @@ class ParticipantAudioInputStream extends import_io.AudioInput {
|
|
|
148
149
|
}
|
|
149
150
|
async close() {
|
|
150
151
|
var _a;
|
|
151
|
-
this.room.off(
|
|
152
|
-
this.room.off(
|
|
153
|
-
this.room.off(
|
|
152
|
+
this.room.off(import_rtc_node.RoomEvent.TrackSubscribed, this.onTrackSubscribed);
|
|
153
|
+
this.room.off(import_rtc_node.RoomEvent.TrackUnpublished, this.onTrackUnpublished);
|
|
154
|
+
this.room.off(import_rtc_node.RoomEvent.TokenRefreshed, this.onTokenRefreshed);
|
|
154
155
|
this.closeStream();
|
|
156
|
+
await super.close();
|
|
155
157
|
(_a = this.frameProcessor) == null ? void 0 : _a.close();
|
|
156
158
|
this.frameProcessor = void 0;
|
|
157
|
-
await this.deferredStream.stream.cancel().catch(() => {
|
|
158
|
-
});
|
|
159
159
|
}
|
|
160
160
|
}
|
|
161
161
|
// Annotate the CommonJS export names for ESM import in node:
|
|
@@ -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 {
|
|
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 {\n type AudioFrame,\n AudioStream,\n FrameProcessor,\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 currentInputId: string | null = null;\n private logger = log();\n\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 if (this.participantIdentity) {\n this.closeStream();\n }\n this.participantIdentity = participantIdentity;\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.currentInputId) {\n void this.multiStream.removeInputStream(this.currentInputId);\n this.currentInputId = null;\n }\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.currentInputId = this.multiStream.addInputStream(\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 override 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 await super.close();\n\n this.frameProcessor?.close();\n this.frameProcessor = undefined;\n }\n}\n"],"mappings":"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","names":["publication"]}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { type AudioFrame, FrameProcessor } from '@livekit/rtc-node';
|
|
2
|
-
import { type NoiseCancellationOptions, RemoteParticipant, type Room } from '@livekit/rtc-node';
|
|
1
|
+
import { type AudioFrame, FrameProcessor, type NoiseCancellationOptions, RemoteParticipant, type Room } from '@livekit/rtc-node';
|
|
3
2
|
import { AudioInput } from '../io.js';
|
|
4
3
|
export declare class ParticipantAudioInputStream extends AudioInput {
|
|
5
4
|
private room;
|
|
@@ -9,6 +8,7 @@ export declare class ParticipantAudioInputStream extends AudioInput {
|
|
|
9
8
|
private frameProcessor?;
|
|
10
9
|
private publication;
|
|
11
10
|
private participantIdentity;
|
|
11
|
+
private currentInputId;
|
|
12
12
|
private logger;
|
|
13
13
|
constructor({ room, sampleRate, numChannels, noiseCancellation, }: {
|
|
14
14
|
room: Room;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { type AudioFrame, FrameProcessor } from '@livekit/rtc-node';
|
|
2
|
-
import { type NoiseCancellationOptions, RemoteParticipant, type Room } from '@livekit/rtc-node';
|
|
1
|
+
import { type AudioFrame, FrameProcessor, type NoiseCancellationOptions, RemoteParticipant, type Room } from '@livekit/rtc-node';
|
|
3
2
|
import { AudioInput } from '../io.js';
|
|
4
3
|
export declare class ParticipantAudioInputStream extends AudioInput {
|
|
5
4
|
private room;
|
|
@@ -9,6 +8,7 @@ export declare class ParticipantAudioInputStream extends AudioInput {
|
|
|
9
8
|
private frameProcessor?;
|
|
10
9
|
private publication;
|
|
11
10
|
private participantIdentity;
|
|
11
|
+
private currentInputId;
|
|
12
12
|
private logger;
|
|
13
13
|
constructor({ room, sampleRate, numChannels, noiseCancellation, }: {
|
|
14
14
|
room: Room;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"_input.d.ts","sourceRoot":"","sources":["../../../src/voice/room_io/_input.ts"],"names":[],"mappings":"AAGA,OAAO,
|
|
1
|
+
{"version":3,"file":"_input.d.ts","sourceRoot":"","sources":["../../../src/voice/room_io/_input.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,KAAK,UAAU,EAEf,cAAc,EACd,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,cAAc,CAAuB;IAC7C,OAAO,CAAC,MAAM,CAAS;gBAEX,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;IA6C7D,OAAO,CAAC,kBAAkB,CAqBxB;IAEF,OAAO,CAAC,WAAW;IASnB,OAAO,CAAC,iBAAiB,CA+BvB;IAEF,OAAO,CAAC,gBAAgB,CAOtB;IAEF,OAAO,CAAC,YAAY;IASL,KAAK;CAUrB"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { FrameProcessor } from "@livekit/rtc-node";
|
|
2
1
|
import {
|
|
3
2
|
AudioStream,
|
|
3
|
+
FrameProcessor,
|
|
4
4
|
RemoteParticipant,
|
|
5
5
|
RoomEvent,
|
|
6
6
|
TrackSource
|
|
@@ -16,6 +16,7 @@ class ParticipantAudioInputStream extends AudioInput {
|
|
|
16
16
|
frameProcessor;
|
|
17
17
|
publication = null;
|
|
18
18
|
participantIdentity = null;
|
|
19
|
+
currentInputId = null;
|
|
19
20
|
logger = log();
|
|
20
21
|
constructor({
|
|
21
22
|
room,
|
|
@@ -81,8 +82,9 @@ class ParticipantAudioInputStream extends AudioInput {
|
|
|
81
82
|
}
|
|
82
83
|
};
|
|
83
84
|
closeStream() {
|
|
84
|
-
if (this.
|
|
85
|
-
this.
|
|
85
|
+
if (this.currentInputId) {
|
|
86
|
+
void this.multiStream.removeInputStream(this.currentInputId);
|
|
87
|
+
this.currentInputId = null;
|
|
86
88
|
}
|
|
87
89
|
this.publication = null;
|
|
88
90
|
}
|
|
@@ -94,7 +96,7 @@ class ParticipantAudioInputStream extends AudioInput {
|
|
|
94
96
|
}
|
|
95
97
|
this.closeStream();
|
|
96
98
|
this.publication = publication;
|
|
97
|
-
this.
|
|
99
|
+
this.currentInputId = this.multiStream.addInputStream(
|
|
98
100
|
resampleStream({
|
|
99
101
|
stream: this.createStream(track),
|
|
100
102
|
outputRate: this.sampleRate
|
|
@@ -134,10 +136,9 @@ class ParticipantAudioInputStream extends AudioInput {
|
|
|
134
136
|
this.room.off(RoomEvent.TrackUnpublished, this.onTrackUnpublished);
|
|
135
137
|
this.room.off(RoomEvent.TokenRefreshed, this.onTokenRefreshed);
|
|
136
138
|
this.closeStream();
|
|
139
|
+
await super.close();
|
|
137
140
|
(_a = this.frameProcessor) == null ? void 0 : _a.close();
|
|
138
141
|
this.frameProcessor = void 0;
|
|
139
|
-
await this.deferredStream.stream.cancel().catch(() => {
|
|
140
|
-
});
|
|
141
142
|
}
|
|
142
143
|
}
|
|
143
144
|
export {
|
|
@@ -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 {
|
|
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 {\n type AudioFrame,\n AudioStream,\n FrameProcessor,\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 currentInputId: string | null = null;\n private logger = log();\n\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 if (this.participantIdentity) {\n this.closeStream();\n }\n this.participantIdentity = participantIdentity;\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.currentInputId) {\n void this.multiStream.removeInputStream(this.currentInputId);\n this.currentInputId = null;\n }\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.currentInputId = this.multiStream.addInputStream(\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 override 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 await super.close();\n\n this.frameProcessor?.close();\n this.frameProcessor = undefined;\n }\n}\n"],"mappings":"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","names":["publication"]}
|
|
@@ -249,6 +249,15 @@ class RoomIO {
|
|
|
249
249
|
get isParticipantAvailable() {
|
|
250
250
|
return this.participantAvailableFuture.done;
|
|
251
251
|
}
|
|
252
|
+
get linkedParticipant() {
|
|
253
|
+
if (!this.isParticipantAvailable) {
|
|
254
|
+
return void 0;
|
|
255
|
+
}
|
|
256
|
+
return this.participantAvailableFuture.result;
|
|
257
|
+
}
|
|
258
|
+
get localParticipant() {
|
|
259
|
+
return this.room.localParticipant ?? void 0;
|
|
260
|
+
}
|
|
252
261
|
/** Switch to a different participant */
|
|
253
262
|
setParticipant(participantIdentity) {
|
|
254
263
|
var _a;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/voice/room_io/room_io.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport {\n type AudioFrame,\n ConnectionState,\n DisconnectReason,\n type FrameProcessor,\n type NoiseCancellationOptions,\n type Participant,\n ParticipantKind,\n type RemoteParticipant,\n type Room,\n RoomEvent,\n type TextStreamInfo,\n type TextStreamReader,\n TrackPublishOptions,\n TrackSource,\n} from '@livekit/rtc-node';\nimport type { WritableStreamDefaultWriter } from 'node:stream/web';\nimport { ATTRIBUTE_PUBLISH_ON_BEHALF, TOPIC_CHAT } from '../../constants.js';\nimport { log } from '../../log.js';\nimport { IdentityTransform } from '../../stream/identity_transform.js';\nimport { Future, Task } from '../../utils.js';\nimport { type AgentSession } from '../agent_session.js';\nimport {\n AgentSessionEventTypes,\n type AgentStateChangedEvent,\n CloseReason,\n type UserInputTranscribedEvent,\n} from '../events.js';\nimport type { AudioOutput, TextOutput } from '../io.js';\nimport { TranscriptionSynchronizer } from '../transcription/synchronizer.js';\nimport { ParticipantAudioInputStream } from './_input.js';\nimport {\n ParalellTextOutput,\n ParticipantAudioOutput,\n ParticipantLegacyTranscriptionOutput,\n ParticipantTranscriptionOutput,\n} from './_output.js';\n\nexport interface TextInputEvent {\n text: string;\n info: TextStreamInfo;\n participant: RemoteParticipant;\n}\n\nexport type TextInputCallback = (sess: AgentSession, ev: TextInputEvent) => void | Promise<void>;\n\nconst DEFAULT_TEXT_INPUT_CALLBACK: TextInputCallback = (sess: AgentSession, ev: TextInputEvent) => {\n sess.interrupt();\n sess.generateReply({ userInput: ev.text });\n};\n\nconst DEFAULT_PARTICIPANT_KINDS: ParticipantKind[] = [\n ParticipantKind.CONNECTOR,\n ParticipantKind.SIP,\n ParticipantKind.STANDARD,\n];\n\nconst CLOSE_ON_DISCONNECT_REASONS: DisconnectReason[] = [\n DisconnectReason.CLIENT_INITIATED,\n DisconnectReason.ROOM_DELETED,\n DisconnectReason.USER_REJECTED,\n];\n\nexport interface RoomInputOptions {\n audioSampleRate: number;\n audioNumChannels: number;\n /** If not given, default to True. */\n textEnabled: boolean;\n /** If not given, default to True. */\n audioEnabled: boolean;\n /** If not given, default to False. */\n videoEnabled: boolean;\n /** The participant to link to. If not provided, link to the first participant.\n Can be overridden by the `participant` argument of RoomIO constructor or `set_participant`.\n */\n participantIdentity?: string;\n noiseCancellation?: NoiseCancellationOptions | FrameProcessor<AudioFrame>;\n textInputCallback?: TextInputCallback;\n /** Participant kinds accepted for auto subscription. If not provided,\n accept `DEFAULT_PARTICIPANT_KINDS`\n */\n participantKinds?: ParticipantKind[];\n /** Close the AgentSession if the linked participant disconnects with reasons in\n CLIENT_INITIATED, ROOM_DELETED, or USER_REJECTED.\n */\n closeOnDisconnect: boolean;\n}\n\nexport interface RoomOutputOptions {\n /** If not given, default to True. */\n transcriptionEnabled: boolean;\n /** If not given, default to True. */\n audioEnabled: boolean;\n audioSampleRate: number;\n audioNumChannels: number;\n /** False to disable transcription synchronization with audio output.\n Otherwise, transcription is emitted as quickly as available.\n */\n syncTranscription: boolean;\n /** The name of the audio track to publish. If not provided, default to \"roomio_audio\".\n */\n audioPublishOptions: TrackPublishOptions;\n}\n\nconst DEFAULT_ROOM_INPUT_OPTIONS: RoomInputOptions = {\n audioSampleRate: 24000,\n audioNumChannels: 1,\n textEnabled: true,\n audioEnabled: true,\n videoEnabled: false,\n textInputCallback: DEFAULT_TEXT_INPUT_CALLBACK,\n closeOnDisconnect: true,\n};\n\nconst DEFAULT_ROOM_OUTPUT_OPTIONS: RoomOutputOptions = {\n audioSampleRate: 24000,\n audioNumChannels: 1,\n transcriptionEnabled: true,\n audioEnabled: true,\n syncTranscription: true,\n audioPublishOptions: new TrackPublishOptions({ source: TrackSource.SOURCE_MICROPHONE }),\n};\n\nexport class RoomIO {\n private agentSession: AgentSession;\n private room: Room;\n private inputOptions: RoomInputOptions;\n private outputOptions: RoomOutputOptions;\n\n private audioInput?: ParticipantAudioInputStream;\n private participantAudioOutput?: ParticipantAudioOutput;\n private userTranscriptOutput?: ParalellTextOutput;\n private agentTranscriptOutput?: ParalellTextOutput;\n private transcriptionSynchronizer?: TranscriptionSynchronizer;\n private participantIdentity: string | null = null;\n\n private participantAvailableFuture: Future<RemoteParticipant> = new Future();\n private roomConnectedFuture: Future<void> = new Future();\n\n // Use stream API for transcript queue\n private userTranscriptStream = new IdentityTransform<UserInputTranscribedEvent>();\n private userTranscriptWriter: WritableStreamDefaultWriter<UserInputTranscribedEvent>;\n private forwardUserTranscriptTask?: Task<void>;\n private initTask?: Task<void>;\n\n private textStreamHandlerRegistered = false;\n\n private logger = log();\n\n constructor({\n agentSession,\n room,\n participant = null,\n inputOptions,\n outputOptions,\n }: {\n agentSession: AgentSession;\n room: Room;\n participant?: RemoteParticipant | string | null;\n inputOptions?: Partial<RoomInputOptions>;\n outputOptions?: Partial<RoomOutputOptions>;\n }) {\n this.agentSession = agentSession;\n this.room = room;\n this.inputOptions = { ...DEFAULT_ROOM_INPUT_OPTIONS, ...inputOptions };\n this.outputOptions = { ...DEFAULT_ROOM_OUTPUT_OPTIONS, ...outputOptions };\n\n this.userTranscriptWriter = this.userTranscriptStream.writable.getWriter();\n\n this.participantIdentity = participant\n ? typeof participant === 'string'\n ? participant\n : participant.identity\n : this.inputOptions.participantIdentity ?? null;\n }\n private async init(signal: AbortSignal): Promise<void> {\n await this.roomConnectedFuture.await;\n\n for (const participant of this.room.remoteParticipants.values()) {\n this.onParticipantConnected(participant);\n }\n if (signal.aborted) {\n return;\n }\n\n const participant = await this.participantAvailableFuture.await;\n this.setParticipant(participant.identity);\n\n // init agent outputs\n this.updateTranscriptionOutput({\n output: this.agentTranscriptOutput,\n participant: this.room.localParticipant?.identity ?? null,\n });\n\n await this.participantAudioOutput?.start(signal);\n }\n\n private onConnectionStateChanged = (state: ConnectionState) => {\n this.logger.debug({ state }, 'connection state changed');\n if (\n state === ConnectionState.CONN_CONNECTED &&\n this.room.isConnected &&\n !this.roomConnectedFuture.done\n ) {\n this.roomConnectedFuture.resolve();\n }\n };\n\n private onParticipantConnected = (participant: RemoteParticipant) => {\n if (this.participantAvailableFuture.done) {\n return;\n }\n\n if (this.participantIdentity) {\n if (participant.identity !== this.participantIdentity) {\n return;\n }\n } else if (\n // otherwise, skip participants that are marked as publishing for this agent\n participant.attributes?.[ATTRIBUTE_PUBLISH_ON_BEHALF] === this.room.localParticipant?.identity\n ) {\n return;\n }\n\n const acceptedKinds = this.inputOptions.participantKinds ?? DEFAULT_PARTICIPANT_KINDS;\n if (participant.info.kind !== undefined && !acceptedKinds.includes(participant.info.kind)) {\n return;\n }\n\n this.participantAvailableFuture.resolve(participant);\n };\n\n private onParticipantDisconnected = (participant: RemoteParticipant) => {\n if (participant.identity !== this.participantIdentity) {\n return;\n }\n this.participantAvailableFuture = new Future<RemoteParticipant>();\n if (\n this.inputOptions.closeOnDisconnect &&\n participant.disconnectReason &&\n CLOSE_ON_DISCONNECT_REASONS.includes(participant.disconnectReason)\n ) {\n this.logger.info(\n {\n participant: participant.identity,\n reason: DisconnectReason[participant.disconnectReason],\n },\n 'closing agent session due to participant disconnect ' +\n '(disable via `RoomInputOptions.closeOnDisconnect=False`)',\n );\n this.agentSession._closeSoon({\n reason: CloseReason.PARTICIPANT_DISCONNECTED,\n });\n }\n };\n\n private onUserInputTranscribed = (ev: UserInputTranscribedEvent) => {\n this.userTranscriptWriter.write(ev).catch((error) => {\n this.logger.error({ error }, 'Failed to write transcript event to stream');\n });\n };\n\n private onAgentStateChanged = async (ev: AgentStateChangedEvent) => {\n if (this.room.isConnected && this.room.localParticipant) {\n await this.room.localParticipant.setAttributes({\n [`lk.agent.state`]: ev.newState,\n });\n }\n };\n\n private onUserTextInput = (reader: TextStreamReader, participantInfo: { identity: string }) => {\n if (participantInfo.identity !== this.participantIdentity) {\n return;\n }\n\n const participant = this.room.remoteParticipants.get(participantInfo.identity);\n if (!participant) {\n this.logger.warn('participant not found, ignoring text input');\n return;\n }\n\n const readText = async () => {\n const text = await reader.readAll();\n\n const textInputResult = this.inputOptions.textInputCallback!(this.agentSession, {\n text,\n info: reader.info,\n participant,\n });\n\n // check if callback is a Promise\n if (textInputResult instanceof Promise) {\n await textInputResult;\n }\n };\n\n readText().catch((error) => {\n this.logger.error({ error }, 'Error reading text input');\n });\n };\n\n private async forwardUserTranscript(signal: AbortSignal): Promise<void> {\n const reader = this.userTranscriptStream.readable.getReader();\n try {\n while (!signal.aborted) {\n const { done, value } = await reader.read();\n if (done) break;\n\n const event = value;\n // IMPORTANT: need to await here to avoid race condition\n await this.userTranscriptOutput?.captureText(event.transcript);\n if (event.isFinal) {\n this.userTranscriptOutput?.flush();\n }\n }\n } catch (error) {\n this.logger.error({ error }, 'Error processing transcript stream');\n }\n }\n\n private createTranscriptionOutput(options: {\n isDeltaStream: boolean;\n participant: Participant | string | null;\n }) {\n return new ParalellTextOutput([\n new ParticipantLegacyTranscriptionOutput(\n this.room,\n options.isDeltaStream,\n options.participant,\n ),\n new ParticipantTranscriptionOutput(this.room, options.isDeltaStream, options.participant),\n ]);\n }\n\n private updateTranscriptionOutput({\n output,\n participant,\n }: {\n output?: ParalellTextOutput;\n participant: string | null;\n }) {\n if (!output) {\n return;\n }\n\n for (const sink of output._sinks) {\n if (\n sink instanceof ParticipantLegacyTranscriptionOutput ||\n sink instanceof ParticipantTranscriptionOutput\n ) {\n sink.setParticipant(participant);\n }\n }\n }\n\n get audioOutput(): AudioOutput | undefined {\n if (!this.transcriptionSynchronizer) {\n return this.participantAudioOutput;\n }\n\n return this.transcriptionSynchronizer.audioOutput;\n }\n\n get transcriptionOutput(): TextOutput | undefined {\n if (!this.transcriptionSynchronizer) {\n return this.agentTranscriptOutput;\n }\n\n return this.transcriptionSynchronizer.textOutput;\n }\n\n get isParticipantAvailable(): boolean {\n return this.participantAvailableFuture.done;\n }\n\n /** Switch to a different participant */\n setParticipant(participantIdentity: string | null) {\n this.logger.debug({ participantIdentity }, 'setting participant');\n if (participantIdentity === null) {\n this.unsetParticipant();\n return;\n }\n\n if (this.participantIdentity !== participantIdentity) {\n this.participantAvailableFuture = new Future<RemoteParticipant>();\n\n // check if new participant is already connected\n for (const participant of this.room.remoteParticipants.values()) {\n if (participant.identity === participantIdentity) {\n this.participantAvailableFuture.resolve(participant);\n break;\n }\n }\n }\n\n // update participant identity and handlers\n this.participantIdentity = participantIdentity;\n this.audioInput?.setParticipant(participantIdentity);\n this.updateTranscriptionOutput({\n output: this.userTranscriptOutput,\n participant: participantIdentity,\n });\n }\n\n unsetParticipant() {\n this.participantIdentity = null;\n this.participantAvailableFuture = new Future<RemoteParticipant>();\n this.audioInput?.setParticipant(null);\n this.updateTranscriptionOutput({\n output: this.userTranscriptOutput,\n participant: null,\n });\n }\n\n start() {\n if (this.inputOptions.textEnabled) {\n try {\n this.room.registerTextStreamHandler(TOPIC_CHAT, this.onUserTextInput);\n this.textStreamHandlerRegistered = true;\n } catch (error) {\n if (this.inputOptions.textEnabled) {\n this.logger.warn(`text stream handler for topic \"${TOPIC_CHAT}\" already set, ignoring`);\n }\n }\n }\n\n // -- create inputs --\n if (this.inputOptions.audioEnabled) {\n this.audioInput = new ParticipantAudioInputStream({\n room: this.room,\n sampleRate: this.inputOptions.audioSampleRate,\n numChannels: this.inputOptions.audioNumChannels,\n noiseCancellation: this.inputOptions.noiseCancellation,\n });\n }\n\n // -- create outputs --\n if (this.outputOptions.audioEnabled) {\n this.participantAudioOutput = new ParticipantAudioOutput(this.room, {\n sampleRate: this.outputOptions.audioSampleRate,\n numChannels: this.outputOptions.audioNumChannels,\n trackPublishOptions: this.outputOptions.audioPublishOptions,\n });\n }\n if (this.outputOptions.transcriptionEnabled) {\n this.userTranscriptOutput = this.createTranscriptionOutput({\n isDeltaStream: false,\n participant: this.participantIdentity,\n });\n // Start the transcript forwarding\n this.forwardUserTranscriptTask = Task.from((controller) =>\n this.forwardUserTranscript(controller.signal),\n );\n this.agentTranscriptOutput = this.createTranscriptionOutput({\n isDeltaStream: true,\n participant: null,\n });\n\n // use the RoomIO's audio output if available, otherwise use the agent's audio output\n // TODO(AJS-176): check for agent output\n const audioOutput = this.participantAudioOutput;\n if (this.outputOptions.syncTranscription && audioOutput) {\n this.transcriptionSynchronizer = new TranscriptionSynchronizer(\n audioOutput,\n this.agentTranscriptOutput,\n );\n }\n }\n\n // -- set the room event handlers --\n this.room.on(RoomEvent.ParticipantConnected, this.onParticipantConnected);\n this.room.on(RoomEvent.ConnectionStateChanged, this.onConnectionStateChanged);\n this.room.on(RoomEvent.ParticipantDisconnected, this.onParticipantDisconnected);\n if (this.room.isConnected) {\n this.onConnectionStateChanged(ConnectionState.CONN_CONNECTED);\n }\n\n this.initTask = Task.from((controller) => this.init(controller.signal));\n\n // attach the agent to the session\n if (this.audioInput) {\n this.agentSession.input.audio = this.audioInput;\n }\n if (this.audioOutput) {\n this.agentSession.output.audio = this.audioOutput;\n }\n if (this.transcriptionOutput) {\n this.agentSession.output.transcription = this.transcriptionOutput;\n }\n\n this.agentSession.on(AgentSessionEventTypes.AgentStateChanged, this.onAgentStateChanged);\n this.agentSession.on(AgentSessionEventTypes.UserInputTranscribed, this.onUserInputTranscribed);\n }\n\n async close() {\n this.room.off(RoomEvent.ParticipantConnected, this.onParticipantConnected);\n this.room.off(RoomEvent.ConnectionStateChanged, this.onConnectionStateChanged);\n this.room.off(RoomEvent.ParticipantDisconnected, this.onParticipantDisconnected);\n this.agentSession.off(AgentSessionEventTypes.UserInputTranscribed, this.onUserInputTranscribed);\n this.agentSession.off(AgentSessionEventTypes.AgentStateChanged, this.onAgentStateChanged);\n\n if (this.textStreamHandlerRegistered) {\n this.room.unregisterTextStreamHandler(TOPIC_CHAT);\n this.textStreamHandlerRegistered = false;\n }\n\n await this.initTask?.cancelAndWait();\n\n // Close stream FIRST so reader.read() in forwardUserTranscript can exit.\n // This is a workaround for a race condition in the stream API.\n this.userTranscriptWriter.close();\n await this.forwardUserTranscriptTask?.cancelAndWait();\n\n await this.audioInput?.close();\n await this.participantAudioOutput?.close();\n await this.transcriptionSynchronizer?.close();\n }\n}\n"],"mappings":"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","names":["participant"]}
|
|
1
|
+
{"version":3,"sources":["../../../src/voice/room_io/room_io.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport {\n type AudioFrame,\n ConnectionState,\n DisconnectReason,\n type FrameProcessor,\n type NoiseCancellationOptions,\n type Participant,\n ParticipantKind,\n type RemoteParticipant,\n type Room,\n RoomEvent,\n type TextStreamInfo,\n type TextStreamReader,\n TrackPublishOptions,\n TrackSource,\n} from '@livekit/rtc-node';\nimport type { WritableStreamDefaultWriter } from 'node:stream/web';\nimport { ATTRIBUTE_PUBLISH_ON_BEHALF, TOPIC_CHAT } from '../../constants.js';\nimport { log } from '../../log.js';\nimport { IdentityTransform } from '../../stream/identity_transform.js';\nimport { Future, Task } from '../../utils.js';\nimport { type AgentSession } from '../agent_session.js';\nimport {\n AgentSessionEventTypes,\n type AgentStateChangedEvent,\n CloseReason,\n type UserInputTranscribedEvent,\n} from '../events.js';\nimport type { AudioOutput, TextOutput } from '../io.js';\nimport { TranscriptionSynchronizer } from '../transcription/synchronizer.js';\nimport { ParticipantAudioInputStream } from './_input.js';\nimport {\n ParalellTextOutput,\n ParticipantAudioOutput,\n ParticipantLegacyTranscriptionOutput,\n ParticipantTranscriptionOutput,\n} from './_output.js';\n\nexport interface TextInputEvent {\n text: string;\n info: TextStreamInfo;\n participant: RemoteParticipant;\n}\n\nexport type TextInputCallback = (sess: AgentSession, ev: TextInputEvent) => void | Promise<void>;\n\nconst DEFAULT_TEXT_INPUT_CALLBACK: TextInputCallback = (sess: AgentSession, ev: TextInputEvent) => {\n sess.interrupt();\n sess.generateReply({ userInput: ev.text });\n};\n\nconst DEFAULT_PARTICIPANT_KINDS: ParticipantKind[] = [\n ParticipantKind.CONNECTOR,\n ParticipantKind.SIP,\n ParticipantKind.STANDARD,\n];\n\nconst CLOSE_ON_DISCONNECT_REASONS: DisconnectReason[] = [\n DisconnectReason.CLIENT_INITIATED,\n DisconnectReason.ROOM_DELETED,\n DisconnectReason.USER_REJECTED,\n];\n\nexport interface RoomInputOptions {\n audioSampleRate: number;\n audioNumChannels: number;\n /** If not given, default to True. */\n textEnabled: boolean;\n /** If not given, default to True. */\n audioEnabled: boolean;\n /** If not given, default to False. */\n videoEnabled: boolean;\n /** The participant to link to. If not provided, link to the first participant.\n Can be overridden by the `participant` argument of RoomIO constructor or `set_participant`.\n */\n participantIdentity?: string;\n noiseCancellation?: NoiseCancellationOptions | FrameProcessor<AudioFrame>;\n textInputCallback?: TextInputCallback;\n /** Participant kinds accepted for auto subscription. If not provided,\n accept `DEFAULT_PARTICIPANT_KINDS`\n */\n participantKinds?: ParticipantKind[];\n /** Close the AgentSession if the linked participant disconnects with reasons in\n CLIENT_INITIATED, ROOM_DELETED, or USER_REJECTED.\n */\n closeOnDisconnect: boolean;\n}\n\nexport interface RoomOutputOptions {\n /** If not given, default to True. */\n transcriptionEnabled: boolean;\n /** If not given, default to True. */\n audioEnabled: boolean;\n audioSampleRate: number;\n audioNumChannels: number;\n /** False to disable transcription synchronization with audio output.\n Otherwise, transcription is emitted as quickly as available.\n */\n syncTranscription: boolean;\n /** The name of the audio track to publish. If not provided, default to \"roomio_audio\".\n */\n audioPublishOptions: TrackPublishOptions;\n}\n\nconst DEFAULT_ROOM_INPUT_OPTIONS: RoomInputOptions = {\n audioSampleRate: 24000,\n audioNumChannels: 1,\n textEnabled: true,\n audioEnabled: true,\n videoEnabled: false,\n textInputCallback: DEFAULT_TEXT_INPUT_CALLBACK,\n closeOnDisconnect: true,\n};\n\nconst DEFAULT_ROOM_OUTPUT_OPTIONS: RoomOutputOptions = {\n audioSampleRate: 24000,\n audioNumChannels: 1,\n transcriptionEnabled: true,\n audioEnabled: true,\n syncTranscription: true,\n audioPublishOptions: new TrackPublishOptions({ source: TrackSource.SOURCE_MICROPHONE }),\n};\n\nexport class RoomIO {\n private agentSession: AgentSession;\n private room: Room;\n private inputOptions: RoomInputOptions;\n private outputOptions: RoomOutputOptions;\n\n private audioInput?: ParticipantAudioInputStream;\n private participantAudioOutput?: ParticipantAudioOutput;\n private userTranscriptOutput?: ParalellTextOutput;\n private agentTranscriptOutput?: ParalellTextOutput;\n private transcriptionSynchronizer?: TranscriptionSynchronizer;\n private participantIdentity: string | null = null;\n\n private participantAvailableFuture: Future<RemoteParticipant> = new Future();\n private roomConnectedFuture: Future<void> = new Future();\n\n // Use stream API for transcript queue\n private userTranscriptStream = new IdentityTransform<UserInputTranscribedEvent>();\n private userTranscriptWriter: WritableStreamDefaultWriter<UserInputTranscribedEvent>;\n private forwardUserTranscriptTask?: Task<void>;\n private initTask?: Task<void>;\n\n private textStreamHandlerRegistered = false;\n\n private logger = log();\n\n constructor({\n agentSession,\n room,\n participant = null,\n inputOptions,\n outputOptions,\n }: {\n agentSession: AgentSession;\n room: Room;\n participant?: RemoteParticipant | string | null;\n inputOptions?: Partial<RoomInputOptions>;\n outputOptions?: Partial<RoomOutputOptions>;\n }) {\n this.agentSession = agentSession;\n this.room = room;\n this.inputOptions = { ...DEFAULT_ROOM_INPUT_OPTIONS, ...inputOptions };\n this.outputOptions = { ...DEFAULT_ROOM_OUTPUT_OPTIONS, ...outputOptions };\n\n this.userTranscriptWriter = this.userTranscriptStream.writable.getWriter();\n\n this.participantIdentity = participant\n ? typeof participant === 'string'\n ? participant\n : participant.identity\n : this.inputOptions.participantIdentity ?? null;\n }\n private async init(signal: AbortSignal): Promise<void> {\n await this.roomConnectedFuture.await;\n\n for (const participant of this.room.remoteParticipants.values()) {\n this.onParticipantConnected(participant);\n }\n if (signal.aborted) {\n return;\n }\n\n const participant = await this.participantAvailableFuture.await;\n this.setParticipant(participant.identity);\n\n // init agent outputs\n this.updateTranscriptionOutput({\n output: this.agentTranscriptOutput,\n participant: this.room.localParticipant?.identity ?? null,\n });\n\n await this.participantAudioOutput?.start(signal);\n }\n\n private onConnectionStateChanged = (state: ConnectionState) => {\n this.logger.debug({ state }, 'connection state changed');\n if (\n state === ConnectionState.CONN_CONNECTED &&\n this.room.isConnected &&\n !this.roomConnectedFuture.done\n ) {\n this.roomConnectedFuture.resolve();\n }\n };\n\n private onParticipantConnected = (participant: RemoteParticipant) => {\n if (this.participantAvailableFuture.done) {\n return;\n }\n\n if (this.participantIdentity) {\n if (participant.identity !== this.participantIdentity) {\n return;\n }\n } else if (\n // otherwise, skip participants that are marked as publishing for this agent\n participant.attributes?.[ATTRIBUTE_PUBLISH_ON_BEHALF] === this.room.localParticipant?.identity\n ) {\n return;\n }\n\n const acceptedKinds = this.inputOptions.participantKinds ?? DEFAULT_PARTICIPANT_KINDS;\n if (participant.info.kind !== undefined && !acceptedKinds.includes(participant.info.kind)) {\n return;\n }\n\n this.participantAvailableFuture.resolve(participant);\n };\n\n private onParticipantDisconnected = (participant: RemoteParticipant) => {\n if (participant.identity !== this.participantIdentity) {\n return;\n }\n this.participantAvailableFuture = new Future<RemoteParticipant>();\n if (\n this.inputOptions.closeOnDisconnect &&\n participant.disconnectReason &&\n CLOSE_ON_DISCONNECT_REASONS.includes(participant.disconnectReason)\n ) {\n this.logger.info(\n {\n participant: participant.identity,\n reason: DisconnectReason[participant.disconnectReason],\n },\n 'closing agent session due to participant disconnect ' +\n '(disable via `RoomInputOptions.closeOnDisconnect=False`)',\n );\n this.agentSession._closeSoon({\n reason: CloseReason.PARTICIPANT_DISCONNECTED,\n });\n }\n };\n\n private onUserInputTranscribed = (ev: UserInputTranscribedEvent) => {\n this.userTranscriptWriter.write(ev).catch((error) => {\n this.logger.error({ error }, 'Failed to write transcript event to stream');\n });\n };\n\n private onAgentStateChanged = async (ev: AgentStateChangedEvent) => {\n if (this.room.isConnected && this.room.localParticipant) {\n await this.room.localParticipant.setAttributes({\n [`lk.agent.state`]: ev.newState,\n });\n }\n };\n\n private onUserTextInput = (reader: TextStreamReader, participantInfo: { identity: string }) => {\n if (participantInfo.identity !== this.participantIdentity) {\n return;\n }\n\n const participant = this.room.remoteParticipants.get(participantInfo.identity);\n if (!participant) {\n this.logger.warn('participant not found, ignoring text input');\n return;\n }\n\n const readText = async () => {\n const text = await reader.readAll();\n\n const textInputResult = this.inputOptions.textInputCallback!(this.agentSession, {\n text,\n info: reader.info,\n participant,\n });\n\n // check if callback is a Promise\n if (textInputResult instanceof Promise) {\n await textInputResult;\n }\n };\n\n readText().catch((error) => {\n this.logger.error({ error }, 'Error reading text input');\n });\n };\n\n private async forwardUserTranscript(signal: AbortSignal): Promise<void> {\n const reader = this.userTranscriptStream.readable.getReader();\n try {\n while (!signal.aborted) {\n const { done, value } = await reader.read();\n if (done) break;\n\n const event = value;\n // IMPORTANT: need to await here to avoid race condition\n await this.userTranscriptOutput?.captureText(event.transcript);\n if (event.isFinal) {\n this.userTranscriptOutput?.flush();\n }\n }\n } catch (error) {\n this.logger.error({ error }, 'Error processing transcript stream');\n }\n }\n\n private createTranscriptionOutput(options: {\n isDeltaStream: boolean;\n participant: Participant | string | null;\n }) {\n return new ParalellTextOutput([\n new ParticipantLegacyTranscriptionOutput(\n this.room,\n options.isDeltaStream,\n options.participant,\n ),\n new ParticipantTranscriptionOutput(this.room, options.isDeltaStream, options.participant),\n ]);\n }\n\n private updateTranscriptionOutput({\n output,\n participant,\n }: {\n output?: ParalellTextOutput;\n participant: string | null;\n }) {\n if (!output) {\n return;\n }\n\n for (const sink of output._sinks) {\n if (\n sink instanceof ParticipantLegacyTranscriptionOutput ||\n sink instanceof ParticipantTranscriptionOutput\n ) {\n sink.setParticipant(participant);\n }\n }\n }\n\n get audioOutput(): AudioOutput | undefined {\n if (!this.transcriptionSynchronizer) {\n return this.participantAudioOutput;\n }\n\n return this.transcriptionSynchronizer.audioOutput;\n }\n\n get transcriptionOutput(): TextOutput | undefined {\n if (!this.transcriptionSynchronizer) {\n return this.agentTranscriptOutput;\n }\n\n return this.transcriptionSynchronizer.textOutput;\n }\n\n get isParticipantAvailable(): boolean {\n return this.participantAvailableFuture.done;\n }\n\n get linkedParticipant(): RemoteParticipant | undefined {\n if (!this.isParticipantAvailable) {\n return undefined;\n }\n\n return this.participantAvailableFuture.result;\n }\n\n get localParticipant(): Participant | undefined {\n return this.room.localParticipant ?? undefined;\n }\n\n /** Switch to a different participant */\n setParticipant(participantIdentity: string | null) {\n this.logger.debug({ participantIdentity }, 'setting participant');\n if (participantIdentity === null) {\n this.unsetParticipant();\n return;\n }\n\n if (this.participantIdentity !== participantIdentity) {\n this.participantAvailableFuture = new Future<RemoteParticipant>();\n\n // check if new participant is already connected\n for (const participant of this.room.remoteParticipants.values()) {\n if (participant.identity === participantIdentity) {\n this.participantAvailableFuture.resolve(participant);\n break;\n }\n }\n }\n\n // update participant identity and handlers\n this.participantIdentity = participantIdentity;\n this.audioInput?.setParticipant(participantIdentity);\n this.updateTranscriptionOutput({\n output: this.userTranscriptOutput,\n participant: participantIdentity,\n });\n }\n\n unsetParticipant() {\n this.participantIdentity = null;\n this.participantAvailableFuture = new Future<RemoteParticipant>();\n this.audioInput?.setParticipant(null);\n this.updateTranscriptionOutput({\n output: this.userTranscriptOutput,\n participant: null,\n });\n }\n\n start() {\n if (this.inputOptions.textEnabled) {\n try {\n this.room.registerTextStreamHandler(TOPIC_CHAT, this.onUserTextInput);\n this.textStreamHandlerRegistered = true;\n } catch (error) {\n if (this.inputOptions.textEnabled) {\n this.logger.warn(`text stream handler for topic \"${TOPIC_CHAT}\" already set, ignoring`);\n }\n }\n }\n\n // -- create inputs --\n if (this.inputOptions.audioEnabled) {\n this.audioInput = new ParticipantAudioInputStream({\n room: this.room,\n sampleRate: this.inputOptions.audioSampleRate,\n numChannels: this.inputOptions.audioNumChannels,\n noiseCancellation: this.inputOptions.noiseCancellation,\n });\n }\n\n // -- create outputs --\n if (this.outputOptions.audioEnabled) {\n this.participantAudioOutput = new ParticipantAudioOutput(this.room, {\n sampleRate: this.outputOptions.audioSampleRate,\n numChannels: this.outputOptions.audioNumChannels,\n trackPublishOptions: this.outputOptions.audioPublishOptions,\n });\n }\n if (this.outputOptions.transcriptionEnabled) {\n this.userTranscriptOutput = this.createTranscriptionOutput({\n isDeltaStream: false,\n participant: this.participantIdentity,\n });\n // Start the transcript forwarding\n this.forwardUserTranscriptTask = Task.from((controller) =>\n this.forwardUserTranscript(controller.signal),\n );\n this.agentTranscriptOutput = this.createTranscriptionOutput({\n isDeltaStream: true,\n participant: null,\n });\n\n // use the RoomIO's audio output if available, otherwise use the agent's audio output\n // TODO(AJS-176): check for agent output\n const audioOutput = this.participantAudioOutput;\n if (this.outputOptions.syncTranscription && audioOutput) {\n this.transcriptionSynchronizer = new TranscriptionSynchronizer(\n audioOutput,\n this.agentTranscriptOutput,\n );\n }\n }\n\n // -- set the room event handlers --\n this.room.on(RoomEvent.ParticipantConnected, this.onParticipantConnected);\n this.room.on(RoomEvent.ConnectionStateChanged, this.onConnectionStateChanged);\n this.room.on(RoomEvent.ParticipantDisconnected, this.onParticipantDisconnected);\n if (this.room.isConnected) {\n this.onConnectionStateChanged(ConnectionState.CONN_CONNECTED);\n }\n\n this.initTask = Task.from((controller) => this.init(controller.signal));\n\n // attach the agent to the session\n if (this.audioInput) {\n this.agentSession.input.audio = this.audioInput;\n }\n if (this.audioOutput) {\n this.agentSession.output.audio = this.audioOutput;\n }\n if (this.transcriptionOutput) {\n this.agentSession.output.transcription = this.transcriptionOutput;\n }\n\n this.agentSession.on(AgentSessionEventTypes.AgentStateChanged, this.onAgentStateChanged);\n this.agentSession.on(AgentSessionEventTypes.UserInputTranscribed, this.onUserInputTranscribed);\n }\n\n async close() {\n this.room.off(RoomEvent.ParticipantConnected, this.onParticipantConnected);\n this.room.off(RoomEvent.ConnectionStateChanged, this.onConnectionStateChanged);\n this.room.off(RoomEvent.ParticipantDisconnected, this.onParticipantDisconnected);\n this.agentSession.off(AgentSessionEventTypes.UserInputTranscribed, this.onUserInputTranscribed);\n this.agentSession.off(AgentSessionEventTypes.AgentStateChanged, this.onAgentStateChanged);\n\n if (this.textStreamHandlerRegistered) {\n this.room.unregisterTextStreamHandler(TOPIC_CHAT);\n this.textStreamHandlerRegistered = false;\n }\n\n await this.initTask?.cancelAndWait();\n\n // Close stream FIRST so reader.read() in forwardUserTranscript can exit.\n // This is a workaround for a race condition in the stream API.\n this.userTranscriptWriter.close();\n await this.forwardUserTranscriptTask?.cancelAndWait();\n\n await this.audioInput?.close();\n await this.participantAudioOutput?.close();\n await this.transcriptionSynchronizer?.close();\n }\n}\n"],"mappings":"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","names":["participant"]}
|