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:
MythEclipse
2026-05-18 05:19:38 +07:00
co-authored by Claude Opus 4.7
parent 3ae7a4c6ad
commit ff425e1f46
4 changed files with 58 additions and 6 deletions
+27 -3
View File
@@ -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);
},
);
}
+20 -2
View File
@@ -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
View File
@@ -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> {
+4
View File
@@ -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",