Skip to content

Commit 755fcf9

Browse files
fix: handle unhandled 'error' event on FfmpegCommand in audio.ts (#1173)
Co-authored-by: Brian Yin <brian.yin@livekit.io>
1 parent f0ac742 commit 755fcf9

4 files changed

Lines changed: 34 additions & 8 deletions

File tree

.changeset/witty-suns-trade.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@livekit/agents": patch
3+
---
4+
5+
fix: handle unhandled 'error' event on FfmpegCommand in audio.ts

agents/src/audio.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import ffmpeg from 'fluent-ffmpeg';
77
import type { ReadableStream } from 'node:stream/web';
88
import { log } from './log.js';
99
import { createStreamChannel } from './stream/stream_channel.js';
10-
import type { AudioBuffer } from './utils.js';
10+
import { type AudioBuffer, isFfmpegTeardownError } from './utils.js';
1111

1212
ffmpeg.setFfmpegPath(ffmpegInstaller.path);
1313

@@ -143,6 +143,17 @@ export function audioFramesFromFile(
143143
}
144144
};
145145

146+
command.on('error', (err: Error) => {
147+
if (isFfmpegTeardownError(err)) {
148+
// Expected during teardown — not an error
149+
logger.debug('FFmpeg command ended during shutdown');
150+
} else {
151+
logger.error(err, 'FFmpeg command error');
152+
}
153+
commandRunning = false;
154+
onClose();
155+
});
156+
146157
const outputStream = command.pipe();
147158
options.abortSignal?.addEventListener('abort', onClose, { once: true });
148159

agents/src/utils.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -753,6 +753,21 @@ export function isStreamClosedError(error: unknown): boolean {
753753
);
754754
}
755755

756+
/** FFmpeg error messages expected during normal teardown/shutdown. */
757+
const FFMPEG_TEARDOWN_ERRORS = ['Output stream closed', 'received signal 2', 'SIGKILL', 'SIGINT'];
758+
759+
/**
760+
* Check if an error is an expected FFmpeg teardown error that can be safely ignored during cleanup.
761+
*
762+
* @param error - The error to check.
763+
* @returns True if the error is an expected FFmpeg shutdown error.
764+
*/
765+
export function isFfmpegTeardownError(error: unknown): boolean {
766+
return (
767+
error instanceof Error && FFMPEG_TEARDOWN_ERRORS.some((msg) => error.message?.includes(msg))
768+
);
769+
}
770+
756771
/**
757772
* In JS an error can be any arbitrary value.
758773
* This function converts an unknown error to an Error and stores the original value in the error object.

agents/src/voice/recorder_io/recorder_io.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { TransformStream } from 'node:stream/web';
1313
import { log } from '../../log.js';
1414
import { isStreamReaderReleaseError } from '../../stream/deferred_stream.js';
1515
import { type StreamChannel, createStreamChannel } from '../../stream/stream_channel.js';
16-
import { Future, Task, cancelAndWait, delay } from '../../utils.js';
16+
import { Future, Task, cancelAndWait, delay, isFfmpegTeardownError } from '../../utils.js';
1717
import type { AgentSession } from '../agent_session.js';
1818
import { AudioInput, AudioOutput, type PlaybackFinishedEvent } from '../io.js';
1919

@@ -203,12 +203,7 @@ export class RecorderIO {
203203
})
204204
.on('error', (err) => {
205205
// Ignore errors from intentional stream closure or SIGINT during shutdown
206-
if (
207-
err.message?.includes('Output stream closed') ||
208-
err.message?.includes('received signal 2') ||
209-
err.message?.includes('SIGKILL') ||
210-
err.message?.includes('SIGINT')
211-
) {
206+
if (isFfmpegTeardownError(err)) {
212207
resolve();
213208
} else {
214209
this.logger.error({ err }, 'FFmpeg encoding error');

0 commit comments

Comments
 (0)