From 2687e5b63bc8d935ab3044491dc1a448769e9962 Mon Sep 17 00:00:00 2001 From: Tyler Gibbs Date: Thu, 23 Jul 2026 16:53:23 -0500 Subject: [PATCH] fix(audio): reject ffplay spawn errors Listen for asynchronous child-process errors so playAudio() rejects when ffplay cannot be started instead of causing an uncaught EventEmitter error. Assisted-by: OpenAI Codex (GPT-5) --- src/helpers/audio.ts | 1 + tests/helpers/audio.test.ts | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/helpers/audio.ts b/src/helpers/audio.ts index 2a6aad3c4..5b9ae5150 100644 --- a/src/helpers/audio.ts +++ b/src/helpers/audio.ts @@ -36,6 +36,7 @@ async function nodejsPlayAudio(stream: NodeJS.ReadableStream | Response | File): return new Promise((resolve, reject) => { try { const ffplay = spawn('ffplay', ['-autoexit', '-nodisp', '-i', 'pipe:0']); + ffplay.on('error', reject); let source: NodeJS.ReadableStream; if (isResponse(stream)) { diff --git a/tests/helpers/audio.test.ts b/tests/helpers/audio.test.ts index 6c2a3be87..c0401fdb9 100644 --- a/tests/helpers/audio.test.ts +++ b/tests/helpers/audio.test.ts @@ -1,6 +1,7 @@ jest.mock('node:child_process', () => ({ spawn: jest.fn() })); import { spawn } from 'node:child_process'; +import { EventEmitter } from 'node:events'; import { Readable, Writable } from 'node:stream'; import { playAudio } from 'openai/helpers/audio'; @@ -72,4 +73,22 @@ describe('playAudio', () => { expect(ffplay.kill).toHaveBeenCalled(); }); + + it('rejects when ffplay cannot be spawned', async () => { + const ffplay = Object.assign(new EventEmitter(), { + stdin: new Writable({ + write(_chunk, _encoding, callback) { + callback(); + }, + }), + kill: jest.fn(), + }); + spawnMock.mockReturnValue(ffplay as any); + + const promise = playAudio(Readable.from(['hello'])); + const error = new Error('spawn ffplay ENOENT'); + + expect(() => ffplay.emit('error', error)).not.toThrow(); + await expect(promise).rejects.toBe(error); + }); });