experiment: ytresolver 제거(poToken 직결) 검증 — 결론 '불가'
4번 안(yt-dlp 단계 제거) 독립 검증용 실험 브랜치. master 에는 병합하지 않는다.
- Config.ytresolverEnabled 플래그 추가(YTRESOLVER_ENABLED, 기본 true=기존 동작).
false 면 resolvePlayable 이 Lavalink youtube-source(poToken) encoded 를 그대로 사용.
- SelfTest 추가: 사람이 음성채널에 없어도 봇이 스스로 입장→재생→position 증가를
확인해 실제 오디오 흐름 여부를 판정(환경변수 미설정 시 비활성).
실측 결과(테스트봇, Redis off, 프로덕션 무영향):
- A) ytresolver ON : 재생 성공, 첫 소리까지 4779ms (오디오 리졸브 3166ms)
- B) ytresolver OFF: 첫 소리까지 1279~1391ms 로 빠르지만 재생 실패.
Lavalink 예외 "All clients failed to load the item." 반복, position 0 고정.
서로 다른 2곡에서 동일 재현 → 곡 특성 아닌 구조적 차단(SABR).
결론: poToken 주입만으로는 스트림 로드가 안 되므로 ytresolver 는 계속 필요.
This commit is contained in:
@@ -261,6 +261,8 @@ export class GuildPlayer {
|
|||||||
* youtube-source가 집 IP의 SABR 차단으로 실패하는 문제를 우회한다. 실패 시 원본 encoded로 폴백. */
|
* youtube-source가 집 IP의 SABR 차단으로 실패하는 문제를 우회한다. 실패 시 원본 encoded로 폴백. */
|
||||||
private async resolvePlayable(track: QueueTrack): Promise<string> {
|
private async resolvePlayable(track: QueueTrack): Promise<string> {
|
||||||
try {
|
try {
|
||||||
|
// 실험: ytresolver 비활성화 시 Lavalink youtube-source(poToken)로 직접 재생 — yt-dlp 단계 생략.
|
||||||
|
if (!Config.ytresolverEnabled) return track.encoded;
|
||||||
const info: any = (track as any).info ?? {};
|
const info: any = (track as any).info ?? {};
|
||||||
const src = info.sourceName;
|
const src = info.sourceName;
|
||||||
if (src !== "youtube" && src !== "spotify" && src !== "youtubemusic") return track.encoded;
|
if (src !== "youtube" && src !== "spotify" && src !== "youtubemusic") return track.encoded;
|
||||||
|
|||||||
@@ -174,6 +174,7 @@ export class LavalinkManager {
|
|||||||
*/
|
*/
|
||||||
public prewarmTrack(track: Track) {
|
public prewarmTrack(track: Track) {
|
||||||
try {
|
try {
|
||||||
|
if (!Config.ytresolverEnabled) return; // yt-dlp 미사용 실험 모드에서는 예열 자체가 불필요
|
||||||
const info: any = (track as any).info ?? {};
|
const info: any = (track as any).info ?? {};
|
||||||
const src = info.sourceName;
|
const src = info.sourceName;
|
||||||
if (src !== "youtube" && src !== "spotify" && src !== "youtubemusic") return;
|
if (src !== "youtube" && src !== "spotify" && src !== "youtubemusic") return;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { client, handler, Redis } from "../index";
|
|||||||
import { Logger } from "../utils/Logger";
|
import { Logger } from "../utils/Logger";
|
||||||
import { DB } from "../utils/Database";
|
import { DB } from "../utils/Database";
|
||||||
import { channelRegister } from "../commands/channel";
|
import { channelRegister } from "../commands/channel";
|
||||||
|
import { runSelfTest } from "../utils/SelfTest";
|
||||||
|
|
||||||
export const clientReady = async () => {
|
export const clientReady = async () => {
|
||||||
if (!client.user) return;
|
if (!client.user) return;
|
||||||
@@ -20,6 +21,9 @@ export const clientReady = async () => {
|
|||||||
Logger.info(`[Redis Pub] bot-guilds 설정 완료: [${guildIds.join(",")}]`);
|
Logger.info(`[Redis Pub] bot-guilds 설정 완료: [${guildIds.join(",")}]`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// [실험 전용] 환경변수가 있을 때만 동작. 프로덕션에는 변수 미설정이라 아무 일도 안 한다.
|
||||||
|
runSelfTest();
|
||||||
|
|
||||||
if (!Config.dev) return;
|
if (!Config.dev) return;
|
||||||
try {
|
try {
|
||||||
const body = Array.from(handler.commands.values().filter(cmd => cmd.visible).map(cmd => cmd.metaData));
|
const body = Array.from(handler.commands.values().filter(cmd => cmd.visible).map(cmd => cmd.metaData));
|
||||||
|
|||||||
@@ -106,6 +106,11 @@ export const Config = {
|
|||||||
// yt-dlp 오디오 리졸버(ytresolver) 주소. 리졸버는 봇/쿠키와 같은 .5 호스트에 co-located.
|
// yt-dlp 오디오 리졸버(ytresolver) 주소. 리졸버는 봇/쿠키와 같은 .5 호스트에 co-located.
|
||||||
ytresolverUrl: (process.env.YTRESOLVER_URL?.trim() || "http://192.168.10.5:8779").replace(/\/+$/, ""),
|
ytresolverUrl: (process.env.YTRESOLVER_URL?.trim() || "http://192.168.10.5:8779").replace(/\/+$/, ""),
|
||||||
|
|
||||||
|
// ytresolver(yt-dlp) 사용 여부. false 면 Lavalink youtube-source(poToken 주입)로 직접 재생한다.
|
||||||
|
// 첫 재생에서 yt-dlp 단계(콜드 3.6~4.2s)를 통째로 없애지만, 집 IP의 SABR/403 차단에
|
||||||
|
// 다시 걸릴 수 있어 실험용 플래그로 둔다. 미설정 시 기존 동작(true) 유지.
|
||||||
|
ytresolverEnabled: (process.env.YTRESOLVER_ENABLED?.trim()?.toLocaleLowerCase() ?? "true") !== "false",
|
||||||
|
|
||||||
proxyUrl: process.env.PROXY_URL?.trim() || "",
|
proxyUrl: process.env.PROXY_URL?.trim() || "",
|
||||||
|
|
||||||
_redis: {
|
_redis: {
|
||||||
|
|||||||
63
bot/src/utils/SelfTest.ts
Normal file
63
bot/src/utils/SelfTest.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { client, lavalinkManager } from "../index";
|
||||||
|
import { channelJoin } from "../commands/join";
|
||||||
|
import { channelRegister } from "../commands/channel";
|
||||||
|
import { Logger } from "./Logger";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [실험 전용] poToken 직결 재생 검증용 자가 테스트.
|
||||||
|
*
|
||||||
|
* SELFTEST_GUILD_ID / SELFTEST_VOICE_CHANNEL_ID 가 설정되면, 봇이 스스로 음성채널에 들어가
|
||||||
|
* 한 곡을 재생한 뒤 실제로 오디오가 흐르는지(position 이 증가하는지) 확인해 결과를 로그로 남긴다.
|
||||||
|
* 사람이 음성채널에 없어도 검증할 수 있게 하기 위한 것으로, 실험 브랜치에서만 사용한다.
|
||||||
|
*/
|
||||||
|
export async function runSelfTest() {
|
||||||
|
const guildId = process.env.SELFTEST_GUILD_ID?.trim();
|
||||||
|
const voiceChannelId = process.env.SELFTEST_VOICE_CHANNEL_ID?.trim();
|
||||||
|
const query = process.env.SELFTEST_QUERY?.trim() || "아이유 좋은 날";
|
||||||
|
if (!guildId || !voiceChannelId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
Logger.info(`[SelfTest] 시작: guild=${guildId} voice=${voiceChannelId} query="${query}"`);
|
||||||
|
const guild = client.guilds.cache.get(guildId) ?? await client.guilds.fetch(guildId).catch(() => null);
|
||||||
|
if (!guild) return Logger.error(`[SelfTest] guild 를 찾을 수 없습니다: ${guildId}`);
|
||||||
|
|
||||||
|
// Lavalink 노드 연결을 기다린다(연결 전이면 channelJoin 이 실패한다).
|
||||||
|
for (let i = 0; i < 60 && !lavalinkManager.hasReadyNode(); i++) {
|
||||||
|
await new Promise((r) => setTimeout(r, 500));
|
||||||
|
}
|
||||||
|
if (!lavalinkManager.hasReadyNode()) return Logger.error(`[SelfTest] Lavalink 노드 연결 대기 실패`);
|
||||||
|
|
||||||
|
// 음악 텍스트 채널이 등록돼 있어야 재생 세션을 만들 수 있다.
|
||||||
|
const textChannelId = process.env.SELFTEST_TEXT_CHANNEL_ID?.trim();
|
||||||
|
if (textChannelId) {
|
||||||
|
const embed = await channelRegister(guild, textChannelId);
|
||||||
|
Logger.info(`[SelfTest] 음악채널 등록: ${embed.data.title ?? "-"}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const t0 = Date.now();
|
||||||
|
const { player } = await channelJoin(guild, voiceChannelId);
|
||||||
|
if (!player) return Logger.error(`[SelfTest] 음성채널 참가 실패`);
|
||||||
|
const tJoin = Date.now();
|
||||||
|
Logger.info(`[SelfTest] 음성접속 완료 ${tJoin - t0}ms`);
|
||||||
|
|
||||||
|
await lavalinkManager.search(guild.id, query, client.user?.id ?? "selftest", player);
|
||||||
|
const tPlay = Date.now();
|
||||||
|
Logger.info(`[SelfTest] 재생 요청까지 ${tPlay - t0}ms (검색+해석+리졸브 ${tPlay - tJoin}ms)`);
|
||||||
|
|
||||||
|
// 실제 오디오가 흐르는지 확인: position 이 시간에 따라 증가해야 한다.
|
||||||
|
const samples: number[] = [];
|
||||||
|
for (let i = 0; i < 6; i++) {
|
||||||
|
await new Promise((r) => setTimeout(r, 1500));
|
||||||
|
samples.push(player.position);
|
||||||
|
Logger.log(`[SelfTest] t+${((i + 1) * 1.5).toFixed(1)}s position=${player.position} isPlaying=${player.isPlaying} track="${player.nowTrack?.info?.title ?? "-"}"`);
|
||||||
|
}
|
||||||
|
const advanced = samples[samples.length - 1] > samples[0] && samples[samples.length - 1] > 0;
|
||||||
|
if (advanced) {
|
||||||
|
Logger.ready(`[SelfTest] 결과: 재생 성공 ✅ (position ${samples[0]} → ${samples[samples.length - 1]}, 첫 소리까지 약 ${tPlay - t0}ms)`);
|
||||||
|
} else {
|
||||||
|
Logger.error(`[SelfTest] 결과: 재생 실패 ❌ (position 이 진행하지 않음: ${samples.join(", ")})`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
Logger.error(`[SelfTest] 예외: ${String(err)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
16
docker-compose.exp.yml
Normal file
16
docker-compose.exp.yml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
services:
|
||||||
|
music-exp:
|
||||||
|
build: ./bot
|
||||||
|
container_name: music-exp
|
||||||
|
restart: "no"
|
||||||
|
env_file:
|
||||||
|
- ./bot/.env
|
||||||
|
environment:
|
||||||
|
- REDIS=false
|
||||||
|
- SELFTEST_GUILD_ID=1352269198297923648
|
||||||
|
- SELFTEST_VOICE_CHANNEL_ID=1352269198914621465
|
||||||
|
- SELFTEST_TEXT_CHANNEL_ID=1421342055774289980
|
||||||
|
- SELFTEST_QUERY=아이유 좋은 날
|
||||||
|
- YTRESOLVER_ENABLED=false
|
||||||
|
volumes:
|
||||||
|
- ./testdb:/db
|
||||||
Reference in New Issue
Block a user