fix: resolve silent voice playback on VPS and queue screenshare streams
- Add ffmpeg stderr monitoring to catch audio decoding errors - Add HTTP timeout parameters (-connect_timeout, -read_timeout) for VPS reliability - Add URL validation in yt-dlp to ensure audio URLs are resolved - Add comprehensive logging to mediaController for queue operations - Add logging to musicPlayer for ffmpeg command execution and errors - Update musicPlayer test to expect new timeout parameters Fixes: 1. Silent audio issue: ffmpeg errors are now visible in logs 2. Queue bypass: Added logging to track queue → playback flow 3. VPS network issues: Added timeouts for HTTP stream connections Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
3ae7a4c6ad
commit
ff425e1f46
@@ -1,3 +1,4 @@
|
||||
import { createChildLogger } from "../logger";
|
||||
import { AppError } from "../errors";
|
||||
import { discordPlayer } from "../player";
|
||||
import { MediaQueue } from "./mediaQueue";
|
||||
@@ -14,6 +15,8 @@ import type {
|
||||
} from "./mediaTypes";
|
||||
import { createMusicPlayer } from "./musicPlayer";
|
||||
|
||||
const logger = createChildLogger("mediaController");
|
||||
|
||||
export interface MediaControllerDependencies {
|
||||
isVoiceConnected?: () => boolean;
|
||||
isBrowserStreaming?: () => boolean;
|
||||
@@ -77,9 +80,14 @@ export class MediaController {
|
||||
): Promise<MediaState> {
|
||||
const mode = options.mode ?? "music";
|
||||
|
||||
logger.info({ source: source.slice(0, 100), mode }, "Queuing media");
|
||||
const resolved = await (
|
||||
this.dependencies.resolveMediaSource ?? resolveMediaSource
|
||||
)(source, mode);
|
||||
logger.info(
|
||||
{ title: resolved.title, kind: resolved.kind },
|
||||
"Media resolved",
|
||||
);
|
||||
|
||||
if (mode === "screen") {
|
||||
// Stop current music if any
|
||||
@@ -90,6 +98,7 @@ export class MediaController {
|
||||
this.queueStore.clear();
|
||||
this.queueStore.add(resolved, mode, options.requestedBy);
|
||||
this.queueStore.startNext();
|
||||
logger.info({ title: resolved.title }, "Starting screen share");
|
||||
return this.startScreen(resolved.source);
|
||||
}
|
||||
|
||||
@@ -111,6 +120,10 @@ export class MediaController {
|
||||
|
||||
this.assertCanStartMusic();
|
||||
this.queueStore.add(resolved, mode, options.requestedBy);
|
||||
logger.info(
|
||||
{ title: resolved.title, queueSize: this.queueStore.snapshot().queue.length },
|
||||
"Added to queue",
|
||||
);
|
||||
this.startNextIfIdle();
|
||||
return this.emitState();
|
||||
}
|
||||
@@ -205,12 +218,20 @@ export class MediaController {
|
||||
private startNextIfIdle(): void {
|
||||
if (this.playback) return;
|
||||
const item = this.queueStore.startNext();
|
||||
if (!item) return;
|
||||
if (!item) {
|
||||
logger.debug("Queue empty, no playback started");
|
||||
return;
|
||||
}
|
||||
|
||||
const token = ++this.playbackToken;
|
||||
logger.info(
|
||||
{ title: item.title, token, queueSize: this.queueStore.snapshot().queue.length },
|
||||
"Starting playback",
|
||||
);
|
||||
try {
|
||||
this.playback = this.musicPlayer.play(item);
|
||||
} catch {
|
||||
} catch (err) {
|
||||
logger.error({ err }, "Failed to start playback");
|
||||
this.queueStore.failCurrent();
|
||||
this.playback = null;
|
||||
this.startNextIfIdle();
|
||||
@@ -220,7 +241,10 @@ export class MediaController {
|
||||
|
||||
this.playback.done.then(
|
||||
() => this.finishCurrent(token, false),
|
||||
() => this.finishCurrent(token, true),
|
||||
(err) => {
|
||||
logger.error({ err, token }, "Playback failed");
|
||||
this.finishCurrent(token, true);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,15 @@ export function createMusicPlayer(
|
||||
const proc = spawn("ffmpeg", buildFfmpegArgs(source.source), {
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
}) as unknown as ChildProcessWithoutNullStreams;
|
||||
proc.stderr.resume();
|
||||
|
||||
let stderrOutput = "";
|
||||
proc.stderr.on("data", (chunk) => {
|
||||
stderrOutput += chunk.toString();
|
||||
const line = chunk.toString().trim();
|
||||
if (line && !line.includes("frame=")) {
|
||||
console.log("[musicPlayer] ffmpeg:", line);
|
||||
}
|
||||
});
|
||||
|
||||
audioPlayer.playStream(proc.stdout, "music", {
|
||||
inputType: StreamType.Raw,
|
||||
@@ -46,10 +54,12 @@ export function createMusicPlayer(
|
||||
|
||||
const done = new Promise<void>((resolve, reject) => {
|
||||
proc.on("error", (error) => {
|
||||
console.error("[musicPlayer] Process error:", error);
|
||||
release();
|
||||
reject(error);
|
||||
});
|
||||
proc.stdout.on("error", (error) => {
|
||||
console.error("[musicPlayer] Stdout error:", error);
|
||||
release();
|
||||
reject(error);
|
||||
});
|
||||
@@ -59,7 +69,12 @@ export function createMusicPlayer(
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
reject(new Error(`ffmpeg exited with code ${code}`));
|
||||
const errorMsg = `ffmpeg exited with code ${code}`;
|
||||
console.error("[musicPlayer]", errorMsg);
|
||||
if (stderrOutput) {
|
||||
console.error("[musicPlayer] ffmpeg stderr:", stderrOutput.slice(-500));
|
||||
}
|
||||
reject(new Error(errorMsg));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -88,6 +103,8 @@ export function buildFfmpegArgs(source: string): string[] {
|
||||
"-user_agent",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36"
|
||||
);
|
||||
args.push("-connect_timeout", "10");
|
||||
args.push("-read_timeout", "30");
|
||||
}
|
||||
|
||||
args.push(
|
||||
@@ -105,5 +122,6 @@ export function buildFfmpegArgs(source: string): string[] {
|
||||
"pipe:1",
|
||||
);
|
||||
|
||||
console.log("[ffmpeg] Command:", "ffmpeg", args.join(" ").slice(0, 200) + "...");
|
||||
return args;
|
||||
}
|
||||
|
||||
+7
-1
@@ -48,7 +48,13 @@ export function createYtDlp(dependencies: YtDlpDependencies = {}): YtDlpClient {
|
||||
"--no-warnings",
|
||||
"--quiet",
|
||||
]);
|
||||
return value.trim().split("\n")[0] || url;
|
||||
const directUrl = value.trim().split("\n")[0];
|
||||
if (!directUrl) {
|
||||
console.warn("[ytdlp] No audio URL returned for:", url);
|
||||
throw new Error(`Failed to resolve audio URL for: ${url}`);
|
||||
}
|
||||
console.log("[ytdlp] Resolved audio URL:", directUrl.slice(0, 100) + "...");
|
||||
return directUrl;
|
||||
},
|
||||
|
||||
async getDirectVideoUrl(url: string): Promise<string> {
|
||||
|
||||
@@ -58,6 +58,10 @@ describe("createMusicPlayer", () => {
|
||||
"warning",
|
||||
"-user_agent",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36",
|
||||
"-connect_timeout",
|
||||
"10",
|
||||
"-read_timeout",
|
||||
"30",
|
||||
"-i",
|
||||
"https://example.com/song.mp3",
|
||||
"-vn",
|
||||
|
||||
Reference in New Issue
Block a user