From ff425e1f4625e9ba23875b32178751e127a0cdf6 Mon Sep 17 00:00:00 2001 From: MythEclipse Date: Mon, 18 May 2026 05:19:38 +0700 Subject: [PATCH] fix: resolve silent voice playback on VPS and queue screenshare streams MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- src/media/mediaController.ts | 30 +++++++++++++++++++++++++++--- src/media/musicPlayer.ts | 22 ++++++++++++++++++++-- src/media/ytdlp.ts | 8 +++++++- tests/media/musicPlayer.test.ts | 4 ++++ 4 files changed, 58 insertions(+), 6 deletions(-) diff --git a/src/media/mediaController.ts b/src/media/mediaController.ts index bec8b3f..1f3736c 100644 --- a/src/media/mediaController.ts +++ b/src/media/mediaController.ts @@ -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 { 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); + }, ); } diff --git a/src/media/musicPlayer.ts b/src/media/musicPlayer.ts index e5c954e..bfc3231 100644 --- a/src/media/musicPlayer.ts +++ b/src/media/musicPlayer.ts @@ -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((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; } diff --git a/src/media/ytdlp.ts b/src/media/ytdlp.ts index 0330fc5..c17d525 100644 --- a/src/media/ytdlp.ts +++ b/src/media/ytdlp.ts @@ -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 { diff --git a/tests/media/musicPlayer.test.ts b/tests/media/musicPlayer.test.ts index 245e4a5..e767020 100644 --- a/tests/media/musicPlayer.test.ts +++ b/tests/media/musicPlayer.test.ts @@ -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",