bot: 음성 WS 끊김 진단 로깅 + 자가 회복
- closed 이벤트에서 code/reason/byRemote 로깅 (4006/4014/4015 등 원인 식별) - 5초 후 봇이 보이스 채널에 남아있으면 player를 새로 만들어 현재 곡을 position 그대로 이어서 재생 (일시정지/볼륨 상태도 복원) - 재접속 실패 시에만 기존처럼 player 정리 - 라우터/네트워크 일시 끊김(4006 세션 만료 등) 시 사용자 체감 끊김 최소화
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { Guild, Message, TextChannel } from "discord.js";
|
import { Guild, Message, TextChannel } from "discord.js";
|
||||||
import { LoadType, Player, Track, TrackEndEvent, TrackStartEvent } from "shoukaku";
|
import { LoadType, Player, Track, TrackEndEvent, TrackStartEvent, type WebSocketClosedEvent } from "shoukaku";
|
||||||
import { client, lavalinkManager, Redis } from "../index";
|
import { client, lavalinkManager, Redis } from "../index";
|
||||||
import { timeFormat } from "../utils/music/Utils";
|
import { timeFormat } from "../utils/music/Utils";
|
||||||
import { default_content, default_embed, default_image, getButtons } from "../utils/music/Config";
|
import { default_content, default_embed, default_image, getButtons } from "../utils/music/Config";
|
||||||
@@ -32,6 +32,10 @@ export class GuildPlayer {
|
|||||||
public msg: Message,
|
public msg: Message,
|
||||||
) {
|
) {
|
||||||
this.player.setGlobalVolume(50);
|
this.player.setGlobalVolume(50);
|
||||||
|
this.attachPlayerListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
private attachPlayerListeners() {
|
||||||
this.player.on("start", (_data: TrackStartEvent) => {
|
this.player.on("start", (_data: TrackStartEvent) => {
|
||||||
// endTimer가 남아있으면 제거 (새 곡 재생 시작)
|
// endTimer가 남아있으면 제거 (새 곡 재생 시작)
|
||||||
if (this.endTimer !== undefined) {
|
if (this.endTimer !== undefined) {
|
||||||
@@ -56,31 +60,35 @@ export class GuildPlayer {
|
|||||||
this.end();
|
this.end();
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Logger.error(`[GuildPlayer] end 이벤트 <EFBFBD><EFBFBD>리 중 에러: ${String(err)}`);
|
Logger.error(`[GuildPlayer] end 이벤트 처리 중 에러: ${String(err)}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.player.on("closed", () => {
|
this.player.on("closed", (data: WebSocketClosedEvent) => {
|
||||||
if (this.isDead) return;
|
if (this.isDead) return;
|
||||||
Logger.info(`[GuildPlayer] 음성 연결이 끊어졌습니다. 재접속을 대기합니다...`);
|
Logger.warn(
|
||||||
|
`[GuildPlayer] 음성 WS 끊김 (code=${data.code}, reason="${data.reason || "(none)"}", byRemote=${data.byRemote}). 5초 후 복구 시도...`,
|
||||||
|
);
|
||||||
// 이전 closed 타이머가 있으면 제거
|
// 이전 closed 타이머가 있으면 제거
|
||||||
if (this.closedTimer !== undefined) {
|
if (this.closedTimer !== undefined) {
|
||||||
clearTimeout(this.closedTimer);
|
clearTimeout(this.closedTimer);
|
||||||
}
|
}
|
||||||
this.closedTimer = setTimeout(() => {
|
this.closedTimer = setTimeout(async () => {
|
||||||
this.closedTimer = undefined;
|
this.closedTimer = undefined;
|
||||||
if (this.isDead) return;
|
if (this.isDead) return;
|
||||||
// 5초가 지났는데도 연결이 복구되지 않았을 때만 방을 나갑니다.
|
|
||||||
|
|
||||||
// 디스코드 방에 내 봇(me)이 없으면 봇을 삭제(delete)한다!
|
// 디스코드 방에 내 봇(me)이 없으면 봇을 삭제(delete)한다!
|
||||||
if (!this.guild.members.me?.voice?.channelId) {
|
const meChannelId = this.guild.members.me?.voice?.channelId;
|
||||||
|
if (!meChannelId) {
|
||||||
Logger.warn(`[GuildPlayer] 음성채널에 봇이 없습니다. player를 초기화합니다.`);
|
Logger.warn(`[GuildPlayer] 음성채널에 봇이 없습니다. player를 초기화합니다.`);
|
||||||
return this.delete();
|
return this.delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
// (1 = CONNECTED, Shoukaku 버전에 따라 연결 상태 체크가 다를 수 있으<EC9E88><EC9CBC> 안전하게 확인)
|
// 봇이 아직 보이스 채널에 남아있으면 자가 회복 시도
|
||||||
if (this.player && this.player.node.state !== 1) {
|
try {
|
||||||
Logger.warn(`[GuildPlayer] 연결 복구 실패. 봇을 퇴장시킵니다.`);
|
await this.reconnect(meChannelId);
|
||||||
return this.delete();
|
} catch (err) {
|
||||||
|
Logger.error(`[GuildPlayer] 음성 재접속 실패: ${String(err)}. player를 초기화합니다.`);
|
||||||
|
this.delete();
|
||||||
}
|
}
|
||||||
}, 5000);
|
}, 5000);
|
||||||
});
|
});
|
||||||
@@ -103,6 +111,62 @@ export class GuildPlayer {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 음성 WS가 끊겼지만 봇이 아직 보이스 채널에 남아있을 때, player를 새로 만들어 현재 곡을 이어서 재생. */
|
||||||
|
private async reconnect(targetChannelId: string) {
|
||||||
|
const currentTrack = this.nowTrack;
|
||||||
|
const currentPosition = this.position;
|
||||||
|
const wasPaused = this.isPaused;
|
||||||
|
const savedVolume = this.player.volume;
|
||||||
|
|
||||||
|
Logger.info(
|
||||||
|
`[GuildPlayer] 음성 재접속 시도 (channel=${targetChannelId}, track="${currentTrack?.info.title ?? "(none)"}", position=${currentPosition}ms, paused=${wasPaused})`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 기존 player listener 해제 + 정리
|
||||||
|
this.player.removeAllListeners();
|
||||||
|
try {
|
||||||
|
await this.player.destroy();
|
||||||
|
} catch (err) {
|
||||||
|
Logger.warn(`[GuildPlayer] 기존 player.destroy 중 에러(무시): ${String(err)}`);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await lavalinkManager.shoukaku.leaveVoiceChannel(this.guild.id);
|
||||||
|
} catch (err) {
|
||||||
|
Logger.warn(`[GuildPlayer] 기존 leaveVoiceChannel 중 에러(무시): ${String(err)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isDead) return;
|
||||||
|
|
||||||
|
// 게이트웨이 측 정리 대기
|
||||||
|
await new Promise((r) => setTimeout(r, 500));
|
||||||
|
if (this.isDead) return;
|
||||||
|
|
||||||
|
// 재접속 (봇이 옮겨졌을 수도 있으니 me의 현재 채널로)
|
||||||
|
const newPlayer = await lavalinkManager.shoukaku.joinVoiceChannel({
|
||||||
|
guildId: this.guild.id,
|
||||||
|
channelId: targetChannelId,
|
||||||
|
shardId: this.guild.shardId,
|
||||||
|
deaf: true,
|
||||||
|
mute: false,
|
||||||
|
});
|
||||||
|
this.player = newPlayer;
|
||||||
|
this.voiceChannelId = targetChannelId;
|
||||||
|
this.attachPlayerListeners();
|
||||||
|
await this.player.setGlobalVolume(savedVolume || 50);
|
||||||
|
|
||||||
|
// 곡이 있었으면 이어서 재생
|
||||||
|
if (currentTrack) {
|
||||||
|
await this.player.playTrack({
|
||||||
|
track: { encoded: currentTrack.encoded },
|
||||||
|
position: currentPosition,
|
||||||
|
paused: wasPaused,
|
||||||
|
});
|
||||||
|
Logger.info(`[GuildPlayer] 음성 재접속 성공. 곡을 이어서 재생합니다.`);
|
||||||
|
} else {
|
||||||
|
Logger.info(`[GuildPlayer] 음성 재접속 성공 (재생 중인 곡 없음).`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private get GDB() {
|
private get GDB() {
|
||||||
if (!this._GDB) this._GDB = DB.guild.get(this.guild.id);
|
if (!this._GDB) this._GDB = DB.guild.get(this.guild.id);
|
||||||
return this._GDB;
|
return this._GDB;
|
||||||
|
|||||||
Reference in New Issue
Block a user