From 1993f84fad1d9f8639ca1bd80979ef1e76efb794 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 31 Mar 2026 16:40:38 +0000 Subject: [PATCH] Fix FFmpeg binary resolution: test binary actually works before using it The ffmpeg-static bundled binary was crashing immediately (exitCode: null, no data produced). Now we run `ffmpeg -version` to verify the binary works before selecting it, with automatic fallback to system ffmpeg. Also logs ffmpeg binary path at info level and captures signal in close event. https://claude.ai/code/session_01EjpEsC2GCsvwbu4n3XC8EE --- src/audio/player.ts | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/src/audio/player.ts b/src/audio/player.ts index 4781771..e5c52c0 100644 --- a/src/audio/player.ts +++ b/src/audio/player.ts @@ -1,4 +1,4 @@ -import { spawn, type ChildProcess } from "node:child_process"; +import { spawn, execSync, type ChildProcess } from "node:child_process"; import { EventEmitter } from "node:events"; import { createRequire } from "node:module"; import { accessSync, chmodSync, constants } from "node:fs"; @@ -25,9 +25,28 @@ function isExecutable(binPath: string): boolean { } } -/** Resolved once at module load — no repeated fs checks per play(). */ -const resolvedFfmpeg: string = - ffmpegPath && isExecutable(ffmpegPath) ? ffmpegPath : "ffmpeg"; +/** Test if an ffmpeg binary actually works by running -version. */ +function ffmpegWorks(bin: string): boolean { + try { + execSync(`"${bin}" -version`, { timeout: 5000, stdio: "pipe" }); + return true; + } catch { + return false; + } +} + +/** Resolved once at module load — prefer bundled ffmpeg-static, fall back to system. */ +const resolvedFfmpeg: string = (() => { + if (ffmpegPath && isExecutable(ffmpegPath) && ffmpegWorks(ffmpegPath)) { + return ffmpegPath; + } + // Fall back to system ffmpeg + if (ffmpegWorks("ffmpeg")) { + return "ffmpeg"; + } + // Last resort: return whatever we have, will fail at runtime with clear error + return ffmpegPath ?? "ffmpeg"; +})(); /** Resolve ffmpeg binary: prefer bundled ffmpeg-static, fall back to system PATH. */ function getFfmpegCommand(): string { @@ -106,7 +125,7 @@ export class AudioPlayer extends EventEmitter { ); const ffmpegBin = getFfmpegCommand(); - this.logger.debug({ ffmpeg: ffmpegBin }, "Using ffmpeg binary"); + this.logger.info({ ffmpeg: ffmpegBin }, "Using ffmpeg binary"); this.ffmpeg = spawn(ffmpegBin, args, { stdio: ["ignore", "pipe", "pipe"] }); let gotFirstData = false; @@ -123,8 +142,8 @@ export class AudioPlayer extends EventEmitter { } }); - this.ffmpeg.on("close", (code) => { - this.logger.info({ exitCode: code, gotData: gotFirstData, framesPlayed: this.framesPlayed }, "FFmpeg process closed"); + this.ffmpeg.on("close", (code, signal) => { + this.logger.info({ exitCode: code, signal, gotData: gotFirstData, framesPlayed: this.framesPlayed }, "FFmpeg process closed"); if (this.sessionId === playSessionId) { this.ffmpeg = null; // Signal frame loop that no more data is coming }