fix: 재생 멈춤(stall) 시 다음 곡으로 자동 진행
노래가 2/3 지점 등에서 버퍼링/멈춘 뒤 다음 곡으로 넘어가지 않고 프리징되던 문제 해결. - stuck 이벤트: 기존 errMsg는 5초 뒤 같은 곡(queue[0])을 다시 재생해 무한 반복될 수 있었음. stopTrack으로 end 이벤트를 유도해 실제 다음 곡으로 넘김. - 재생 위치 멈춤 감시(watchdog) 추가: TrackStuckEvent가 안 뜨는 느린 버퍼링 상황에서도 position이 20초간 진행 없으면 자동 스킵. start에서 시작, clearAllTimers에서 정리. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,9 @@ import { Logger } from "../utils/Logger";
|
|||||||
|
|
||||||
const DelayAfterErrMs = 1000 * 5;
|
const DelayAfterErrMs = 1000 * 5;
|
||||||
const idleEndTime = 1000 * 60 * 10;
|
const idleEndTime = 1000 * 60 * 10;
|
||||||
|
// 재생 위치가 진행되지 않는지 감시하는 주기 / 멈춤으로 판단하는 임계값
|
||||||
|
const StallCheckIntervalMs = 1000 * 5;
|
||||||
|
const StallThresholdMs = 1000 * 20;
|
||||||
|
|
||||||
type QueueTrack = Track & { userId: string; };
|
type QueueTrack = Track & { userId: string; };
|
||||||
|
|
||||||
@@ -23,6 +26,9 @@ export class GuildPlayer {
|
|||||||
private errorTimer: NodeJS.Timeout | undefined;
|
private errorTimer: NodeJS.Timeout | undefined;
|
||||||
private endTimer: NodeJS.Timeout | undefined;
|
private endTimer: NodeJS.Timeout | undefined;
|
||||||
private closedTimer: NodeJS.Timeout | undefined;
|
private closedTimer: NodeJS.Timeout | undefined;
|
||||||
|
private stallWatchdog: NodeJS.Timeout | undefined;
|
||||||
|
private lastPosition = 0;
|
||||||
|
private lastProgressAt = 0;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public guild: Guild,
|
public guild: Guild,
|
||||||
@@ -42,6 +48,8 @@ export class GuildPlayer {
|
|||||||
clearTimeout(this.endTimer);
|
clearTimeout(this.endTimer);
|
||||||
this.endTimer = undefined;
|
this.endTimer = undefined;
|
||||||
}
|
}
|
||||||
|
// 새 곡이 시작되면 재생 위치 멈춤 감시 시작
|
||||||
|
this.startStallWatchdog();
|
||||||
Redis?.publishState("player_update", {
|
Redis?.publishState("player_update", {
|
||||||
guildId: this.guild.id,
|
guildId: this.guild.id,
|
||||||
});
|
});
|
||||||
@@ -103,8 +111,10 @@ export class GuildPlayer {
|
|||||||
});
|
});
|
||||||
this.player.on("stuck", async (data) => {
|
this.player.on("stuck", async (data) => {
|
||||||
try {
|
try {
|
||||||
Logger.error(`[Lavalink] 곡 로딩 멈춤(Stuck) 발생: ${data.thresholdMs}ms 초과`);
|
Logger.error(`[Lavalink] 곡 로딩 멈춤(Stuck) 발생: ${data.thresholdMs}ms 초과. 다음 곡으로 넘어갑니다.`);
|
||||||
await this.errMsg("음원 로딩이 멈췄습니다. 다음 곡으로 넘어갑니다.");
|
// errMsg는 5초 뒤 같은 곡(queue[0])을 다시 재생해 무한 반복될 수 있으므로,
|
||||||
|
// stopTrack으로 end 이벤트를 유도해 실제로 다음 곡으로 넘긴다.
|
||||||
|
await this.player.stopTrack();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Logger.error(`[GuildPlayer] stuck 이벤트 처리 중 에러: ${String(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() {
|
private clearAllTimers() {
|
||||||
if (this.errorTimer !== undefined) {
|
if (this.errorTimer !== undefined) {
|
||||||
clearTimeout(this.errorTimer);
|
clearTimeout(this.errorTimer);
|
||||||
@@ -388,6 +444,7 @@ export class GuildPlayer {
|
|||||||
clearTimeout(this.closedTimer);
|
clearTimeout(this.closedTimer);
|
||||||
this.closedTimer = undefined;
|
this.closedTimer = undefined;
|
||||||
}
|
}
|
||||||
|
this.stopStallWatchdog();
|
||||||
}
|
}
|
||||||
|
|
||||||
public end() {
|
public end() {
|
||||||
|
|||||||
Reference in New Issue
Block a user