feat(stream): STREAM_BROWSER flag + make toolbar-hide/subtitles broadcast-wide

- Add STREAM_BROWSER (.env) gating screen-share/browser mode. false => the
  /자비스 stream command stays voice + API/MCP only (no Go-Live); true (default)
  => screen share as before. (Browser-driven info retrieval in true mode is a
  follow-up build; the bot has no browser-control tools yet.)
- Make the two test-time fixes broadcast-wide defaults via broadcast-helper.mjs:
  it now also watches every tab for HTML5 fullscreen and toggles Chrome window
  fullscreen so the address bar is hidden for ANY video (xfwm4 won't hide it on
  'f' alone), restoring on exit. Subtitles were already enforced per video.
  scenario.mjs drops its own fullscreen toggle and relies on the helper.
- Revert the test-settings env vars from .env.example (not wanted).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
javis-bot
2026-06-10 16:17:29 +09:00
parent f93b241575
commit ef6f6ff57d
5 changed files with 51 additions and 34 deletions

View File

@@ -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
# ---------------------------------------------------------------------------

View File

@@ -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.

View File

@@ -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);

View File

@@ -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", ""),

View File

@@ -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("셀프봇 송출은 음성 채널 안에서 호출해야 합니다. 음성 채널에 들어간 뒤 다시 시도하세요.");
}