diff --git a/.env.example b/.env.example index 2818d15..acd2984 100644 --- a/.env.example +++ b/.env.example @@ -54,6 +54,13 @@ VNC_PASSWORD=javis123 # Auto-opened page in the in-container Chrome. CHROME_START_URL=about:blank +# --------------------------------------------------------------------------- +# Screen-share + browser mode. +# true = the bot may go Live (screen-share the VNC desktop) and drive the +# on-screen browser for real-time info (search / play / read screen). +# false = no screen share; voice only, real-time info via API/MCP tools. +STREAM_BROWSER=true + # --------------------------------------------------------------------------- # VNC screen broadcast # selfbot = real live "Go Live" stream (needs a USER/burner token; ToS risk) @@ -91,20 +98,6 @@ NOVNC_URL= # --- screenshot backend --- SCREENSHOT_INTERVAL_SEC=5 -# --- stream-test scripts (manual broadcast verification) --- -# Chrome remote-debugging port the scenario/helper attach to. -CDP_PORT=9222 -# How long stream-hold.ts keeps the Go-Live up before auto-stopping (ms, 2h). -HOLD_MS=7200000 -# scenario.mjs inputs: fixed concert clip + search queries. -TEST_VIDEO_ID=X_am71G6Vy4 -TEST_YT_QUERY=내손을잡아 -TEST_NAVER_QUERY=아이유 -# Set MV_QUERY to auto-pick the first >=60fps search result instead of the clip. -MV_QUERY= -# Windowed/fullscreen watch duration per step (seconds). -WATCH_SECONDS=20 - # --------------------------------------------------------------------------- # Voice behaviour # --------------------------------------------------------------------------- diff --git a/bot/scripts/stream-test/broadcast-helper.mjs b/bot/scripts/stream-test/broadcast-helper.mjs index 6209f7c..1312176 100644 --- a/bot/scripts/stream-test/broadcast-helper.mjs +++ b/bot/scripts/stream-test/broadcast-helper.mjs @@ -91,8 +91,37 @@ async function session() { const ctx = b.contexts()[0]; for (const p of ctx.pages()) await arm(p); ctx.on('page', arm); // new tabs + + // Broadcast-wide: when any tab enters HTML5 fullscreen (a video 'f'), hide + // Chrome's toolbar by putting the WINDOW into Chrome-initiated fullscreen - + // xfwm4 won't hide it on HTML5 fullscreen alone, so the address bar would + // otherwise show on the broadcast. Restore when fullscreen exits. + let winFs = false; + const cdp = await b.newBrowserCDPSession(); + const setWindowFs = async (fs) => { + try { + const { targetInfos } = await cdp.send('Target.getTargets'); + const t = targetInfos.find((x) => x.type === 'page' && x.url.startsWith('http')); + if (!t) return; + const { windowId } = await cdp.send('Browser.getWindowForTarget', { targetId: t.targetId }); + await cdp.send('Browser.setWindowBounds', { windowId, bounds: { windowState: fs ? 'fullscreen' : 'normal' } }); + winFs = fs; + } catch { /* best-effort */ } + }; + const fsTimer = setInterval(async () => { + try { + let anyFs = false; + for (const p of ctx.pages()) { + if (await p.evaluate(() => !!document.fullscreenElement).catch(() => false)) { anyFs = true; break; } + } + if (anyFs && !winFs) await setWindowFs(true); + else if (!anyFs && winFs) await setWindowFs(false); + } catch { /* best-effort */ } + }, 600); + console.log('broadcast-helper armed on', ctx.pages().length, 'tab(s)'); await new Promise((resolve) => b.on('disconnected', resolve)); + clearInterval(fsTimer); } // Reconnect across Chrome restarts so the broadcast stays ad-free. diff --git a/bot/scripts/stream-test/scenario.mjs b/bot/scripts/stream-test/scenario.mjs index d872308..9ddbacf 100644 --- a/bot/scripts/stream-test/scenario.mjs +++ b/bot/scripts/stream-test/scenario.mjs @@ -38,19 +38,6 @@ page.setDefaultTimeout(25000); const read = (fn) => page.evaluate(fn); const playerLoc = () => page.locator('#movie_player'); -// Toggle Chrome-initiated browser fullscreen (hides the toolbar on this WM, -// which HTML5 'f' fullscreen alone does not). on=true -> fullscreen. -async function browserFullscreen(on) { - try { - const sess = await b.newBrowserCDPSession(); - const { targetInfos } = await sess.send('Target.getTargets'); - const t = targetInfos.find((x) => x.type === 'page' && x.url.startsWith('http')); - if (!t) return; - const { windowId } = await sess.send('Browser.getWindowForTarget', { targetId: t.targetId }); - await sess.send('Browser.setWindowBounds', { windowId, bounds: { windowState: on ? 'fullscreen' : 'normal' } }); - } catch { /* best-effort */ } -} - const fpsNow = () => read(() => { try { const s = document.getElementById('movie_player').getStatsForNerds(); const m = (s.resolution || '').match(/@(\d+)/); return m ? +m[1] : null; } catch { return null; } }); @@ -129,17 +116,16 @@ if ((await auto.count().catch(() => 0)) && (await auto.getAttribute('aria-checke } console.log('STEP watch-1080-windowed'); await sleep(WATCH_MS); -// 7) fullscreen: hide the browser toolbar (CDP), then the real 'f' key makes the -// video fill the now toolbar-free screen (innerHeight 1080). -await browserFullscreen(true); await sleep(800); +// 7) fullscreen with the real 'f' key. broadcast-helper.mjs detects the HTML5 +// fullscreen and hides Chrome's toolbar (window fullscreen) broadcast-wide, so +// the address bar stays off the stream. await humanHover(page, playerLoc()); -await humanKey('f'); await sleep(1500); -if (!(await read(() => !!document.fullscreenElement))) { await humanHover(page, playerLoc()); await humanKey('f'); await sleep(1200); } +await humanKey('f'); await sleep(1800); +if (!(await read(() => !!document.fullscreenElement))) { await humanHover(page, playerLoc()); await humanKey('f'); await sleep(1500); } console.log('STEP fullscreen', await read(() => ({ full: !!document.fullscreenElement, h: window.innerHeight }))); await sleep(WATCH_MS); -// 8) exit video fullscreen ('f'), then restore the browser toolbar +// 8) exit video fullscreen ('f'); the helper restores the toolbar await humanKey('f'); await sleep(1500); -await browserFullscreen(false); await sleep(500); // 9) Naver via the address bar, then really type the query await navigateOmnibox('https://www.naver.com'); await sleep(2800); diff --git a/bot/src/config.ts b/bot/src/config.ts index 7f2e861..c70c3cc 100644 --- a/bot/src/config.ts +++ b/bot/src/config.ts @@ -47,6 +47,10 @@ export const config = { // STREAM_AUDIO=0 to mute; STREAM_AUDIO_SOURCE overrides the capture source. streamAudio: opt("STREAM_AUDIO", "1") !== "0", streamAudioSource: opt("STREAM_AUDIO_SOURCE", "@DEFAULT_MONITOR@"), + // Screen-share + browser mode. true = the bot may go Live (Go-Live screen + // share of the VNC desktop) and drive the on-screen browser for real-time + // info. false = no screen share; use voice + API/MCP tools for info only. + screenBrowser: opt("STREAM_BROWSER", "true") !== "false", // novnc backend novncUrl: opt("NOVNC_URL", ""), diff --git a/bot/src/index.ts b/bot/src/index.ts index 9ecf9c7..8daf65c 100644 --- a/bot/src/index.ts +++ b/bot/src/index.ts @@ -111,6 +111,11 @@ async function handleStream(i: ChatInputCommandInteraction) { } }, }; + if (!config.screenBrowser) { + return i.editReply( + "화면 공유(브라우저) 모드가 꺼져 있습니다 (STREAM_BROWSER=false). 음성 + API/MCP 모드로만 동작합니다.", + ); + } if (config.streamBackend === "selfbot" && !ctx.voiceChannelId) { return i.editReply("셀프봇 송출은 음성 채널 안에서 호출해야 합니다. 음성 채널에 들어간 뒤 다시 시도하세요."); }