


























@@ -1,3 +1,4 @@
1+import { spawn } from "node:child_process";
12import fs from "node:fs/promises";
23import { Transform, type Readable, type TransformCallback } from "node:stream";
34import {
@@ -7,22 +8,26 @@ import {
78type OpusDecoderHandle as LibopusDecoder,
89type OpusEncoderHandle as LibopusEncoder,
910} from "libopus-wasm";
11+import { resolveFfmpegBin } from "openclaw/plugin-sdk/media-runtime";
1012import { resamplePcm } from "openclaw/plugin-sdk/realtime-voice";
1113import { logVerbose, shouldLogVerbose } from "openclaw/plugin-sdk/runtime-env";
1214import { formatErrorMessage } from "openclaw/plugin-sdk/ssrf-runtime";
1315import { tempWorkspace, resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path";
14-import prism from "prism-media";
15161617const SAMPLE_RATE = 48_000;
1718const CHANNELS = 2;
1819const BIT_DEPTH = 16;
20+const FFMPEG_ERROR_OUTPUT_BYTES = 8_192;
1921const DISCORD_OPUS_FRAME_SIZE = 960;
2022const DISCORD_OPUS_FRAME_BYTES = DISCORD_OPUS_FRAME_SIZE * CHANNELS * (BIT_DEPTH / 8);
2123const FFMPEG_PCM_ARGUMENTS = [
2224"-analyzeduration",
2325"0",
2426"-loglevel",
25-"0",
27+"error",
28+"-vn",
29+"-sn",
30+"-dn",
2631"-f",
2732"s16le",
2833"-ar",
@@ -96,18 +101,58 @@ export function createDiscordOpusEncodeStream(): Transform {
96101}
9710298103export function createDiscordOpusPlaybackStream(input: Readable | string): Readable {
99-const ffmpeg = new prism.FFmpeg({
100-args: ["-i", typeof input === "string" ? input : "-", ...FFMPEG_PCM_ARGUMENTS],
104+const inputSource = typeof input === "string" ? input : "pipe:0";
105+const ffmpeg = spawn(resolveFfmpegBin(), ["-i", inputSource, ...FFMPEG_PCM_ARGUMENTS, "pipe:1"], {
106+stdio: ["pipe", "pipe", "pipe"],
107+windowsHide: true,
101108});
102109const opusStream = createDiscordOpusEncodeStream();
110+let stderr = "";
111+let ffmpegClosed = false;
112+113+ffmpeg.stderr.setEncoding("utf8");
114+ffmpeg.stderr.on("data", (chunk: string) => {
115+if (stderr.length < FFMPEG_ERROR_OUTPUT_BYTES) {
116+stderr = `${stderr}${chunk}`.slice(0, FFMPEG_ERROR_OUTPUT_BYTES);
117+}
118+});
119+120+ffmpeg.once("error", (err) => {
121+opusStream.destroy(err);
122+});
123+ffmpeg.once("close", (code, signal) => {
124+ffmpegClosed = true;
125+if (code && code !== 0) {
126+const suffix = stderr.trim() ? `: ${stderr.trim()}` : "";
127+opusStream.destroy(new Error(`ffmpeg exited with code ${code}${suffix}`));
128+return;
129+}
130+if (signal) {
131+opusStream.destroy(new Error(`ffmpeg exited with signal ${signal}`));
132+}
133+});
103134104-ffmpeg.on("error", (err) => opusStream.destroy(err));
105-opusStream.on("close", () => ffmpeg.destroy());
135+ffmpeg.stdout.on("error", (err) => opusStream.destroy(err));
136+ffmpeg.stdin.on("error", (err) => {
137+if ((err as NodeJS.ErrnoException).code !== "EPIPE") {
138+opusStream.destroy(err);
139+}
140+});
141+ffmpeg.stdout.pipe(opusStream);
142+opusStream.once("close", () => {
143+if (!ffmpegClosed && !opusStream.readableEnded) {
144+ffmpeg.kill();
145+}
146+});
106147if (typeof input !== "string") {
107-input.on("error", (err) => ffmpeg.destroy(err));
108-input.pipe(ffmpeg);
148+input.on("error", (err) => {
149+ffmpeg.stdin.destroy(err);
150+opusStream.destroy(err);
151+});
152+input.pipe(ffmpeg.stdin);
153+} else {
154+ffmpeg.stdin.end();
109155}
110-ffmpeg.pipe(opusStream);
111156return opusStream;
112157}
113158此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。