From 6d79655181c900ef96ca4c812e5d22aaa785cd3f Mon Sep 17 00:00:00 2001 From: tkrmagid Date: Mon, 17 Aug 2026 21:55:05 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EC=9E=AC=EC=83=9D=20=EB=A9=88=EC=B6=A4(?= =?UTF-8?q?stall)=20=EC=8B=9C=20=EB=8B=A4=EC=9D=8C=20=EA=B3=A1=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=9E=90=EB=8F=99=20=EC=A7=84=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 노래가 2/3 지점 등에서 버퍼링/멈춘 뒤 다음 곡으로 넘어가지 않고 프리징되던 문제 해결. - stuck 이벤트: 기존 errMsg는 5초 뒤 같은 곡(queue[0])을 다시 재생해 무한 반복될 수 있었음. stopTrack으로 end 이벤트를 유도해 실제 다음 곡으로 넘김. - 재생 위치 멈춤 감시(watchdog) 추가: TrackStuckEvent가 안 뜨는 느린 버퍼링 상황에서도 position이 20초간 진행 없으면 자동 스킵. start에서 시작, clearAllTimers에서 정리. Co-Authored-By: Claude Opus 4.7 --- bot/src/classes/GuildPlayer.ts | 61 ++++++++++++++++++++++++++++++++-- 1 file changed, 59 insertions(+), 2 deletions(-) diff --git a/bot/src/classes/GuildPlayer.ts b/bot/src/classes/GuildPlayer.ts index 13ac646..c40083b 100644 --- a/bot/src/classes/GuildPlayer.ts +++ b/bot/src/classes/GuildPlayer.ts @@ -11,6 +11,9 @@ import { Logger } from "../utils/Logger"; const DelayAfterErrMs = 1000 * 5; const idleEndTime = 1000 * 60 * 10; +// 재생 위치가 진행되지 않는지 감시하는 주기 / 멈춤으로 판단하는 임계값 +const StallCheckIntervalMs = 1000 * 5; +const StallThresholdMs = 1000 * 20; type QueueTrack = Track & { userId: string; }; @@ -23,6 +26,9 @@ export class GuildPlayer { private errorTimer: NodeJS.Timeout | undefined; private endTimer: NodeJS.Timeout | undefined; private closedTimer: NodeJS.Timeout | undefined; + private stallWatchdog: NodeJS.Timeout | undefined; + private lastPosition = 0; + private lastProgressAt = 0; constructor( public guild: Guild, @@ -42,6 +48,8 @@ export class GuildPlayer { clearTimeout(this.endTimer); this.endTimer = undefined; } + // 새 곡이 시작되면 재생 위치 멈춤 감시 시작 + this.startStallWatchdog(); Redis?.publishState("player_update", { guildId: this.guild.id, }); @@ -103,8 +111,10 @@ export class GuildPlayer { }); this.player.on("stuck", async (data) => { try { - Logger.error(`[Lavalink] 곡 로딩 멈춤(Stuck) 발생: ${data.thresholdMs}ms 초과`); - await this.errMsg("음원 로딩이 멈췄습니다. 다음 곡으로 넘어갑니다."); + Logger.error(`[Lavalink] 곡 로딩 멈춤(Stuck) 발생: ${data.thresholdMs}ms 초과. 다음 곡으로 넘어갑니다.`); + // errMsg는 5초 뒤 같은 곡(queue[0])을 다시 재생해 무한 반복될 수 있으므로, + // stopTrack으로 end 이벤트를 유도해 실제로 다음 곡으로 넘긴다. + await this.player.stopTrack(); } catch (err) { Logger.error(`[GuildPlayer] stuck 이벤트 처리 중 에러: ${String(err)}`); } @@ -375,6 +385,52 @@ export class GuildPlayer { } } + /** + * 재생 위치가 일정 시간 동안 전혀 진행되지 않으면(=버퍼링/멈춤) 다음 곡으로 넘긴다. + * Lavalink가 TrackStuckEvent를 못 던지는 트리클 스트리밍(느린 버퍼링) 상황까지 커버하는 백스탑. + */ + private startStallWatchdog() { + this.stopStallWatchdog(); + this.lastPosition = this.position; + this.lastProgressAt = Date.now(); + this.stallWatchdog = setInterval(() => { + if (this.isDead) { + this.stopStallWatchdog(); + return; + } + // 재생 중이 아니거나 일시정지면 정상 상태 → 진행시각 리셋 + if (!this.isPlaying || this.isPaused) { + this.lastPosition = this.player.position; + this.lastProgressAt = Date.now(); + return; + } + const pos = this.player.position; + if (pos > this.lastPosition + 500) { + // 정상적으로 재생 위치가 진행됨 + this.lastPosition = pos; + this.lastProgressAt = Date.now(); + return; + } + // 재생 위치가 StallThresholdMs 동안 전혀 진행되지 않음 → 멈춤으로 판단하고 스킵 + if (Date.now() - this.lastProgressAt >= StallThresholdMs) { + Logger.error(`[GuildPlayer] 재생 위치가 ${StallThresholdMs}ms 동안 진행되지 않음(멈춤 감지). 다음 곡으로 넘어갑니다. (position=${pos}ms)`); + this.lastPosition = pos; + this.lastProgressAt = Date.now(); + // stopTrack → end 이벤트 → 다음 곡 진행 + this.player.stopTrack().catch((err) => { + Logger.error(`[GuildPlayer] 멈춤 감지 후 stopTrack 에러: ${String(err)}`); + }); + } + }, StallCheckIntervalMs); + } + + private stopStallWatchdog() { + if (this.stallWatchdog !== undefined) { + clearInterval(this.stallWatchdog); + this.stallWatchdog = undefined; + } + } + private clearAllTimers() { if (this.errorTimer !== undefined) { clearTimeout(this.errorTimer); @@ -388,6 +444,7 @@ export class GuildPlayer { clearTimeout(this.closedTimer); this.closedTimer = undefined; } + this.stopStallWatchdog(); } public end() {