fix(stream): tie broadcast-helper to the stream lifecycle, enforce STREAM_BROWSER, fix fullscreen window
Addresses review of the STREAM_BROWSER / broadcast-defaults work: - SelfbotStreamer now spawns broadcast-helper.mjs on stream start and kills it on stop/self-end (alongside capture + keepalive). The ad-skip, subtitle rule and fullscreen-toolbar-hide are therefore guaranteed broadcast-wide defaults tied to the broadcast - not a manual process. Fail-open: if node/Chrome deps are absent the stream runs without the helper. Verified the helper is a child of the broadcast holder and armed. - Enforce STREAM_BROWSER at the streamer (start() returns early when screenBrowser===false), so EVERY caller including stream-hold.ts is voice-only when it's off, not just the slash command. stream-hold.ts reads STREAM_BROWSER. - Fix broadcast-helper fullscreen: resolve the window of the tab actually in HTML5 fullscreen (via its CDP targetId) instead of the first HTTP tab, so the right Chrome window is toggled when multiple windows exist. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -92,30 +92,35 @@ async function session() {
|
|||||||
for (const p of ctx.pages()) await arm(p);
|
for (const p of ctx.pages()) await arm(p);
|
||||||
ctx.on('page', arm); // new tabs
|
ctx.on('page', arm); // new tabs
|
||||||
|
|
||||||
// Broadcast-wide: when any tab enters HTML5 fullscreen (a video 'f'), hide
|
// Broadcast-wide: when a tab enters HTML5 fullscreen (a video 'f'), hide
|
||||||
// Chrome's toolbar by putting the WINDOW into Chrome-initiated fullscreen -
|
// Chrome's toolbar by putting THAT tab's window into Chrome-initiated
|
||||||
// xfwm4 won't hide it on HTML5 fullscreen alone, so the address bar would
|
// fullscreen - xfwm4 won't hide it on HTML5 fullscreen alone, so the address
|
||||||
// otherwise show on the broadcast. Restore when fullscreen exits.
|
// bar would otherwise show on the broadcast. We resolve the exact window of
|
||||||
let winFs = false;
|
// the fullscreen tab (not just the first tab) and restore it on exit.
|
||||||
const cdp = await b.newBrowserCDPSession();
|
const cdp = await b.newBrowserCDPSession();
|
||||||
const setWindowFs = async (fs) => {
|
let fsWindowId = null;
|
||||||
|
const windowIdFor = async (page) => {
|
||||||
|
const s = await page.context().newCDPSession(page);
|
||||||
try {
|
try {
|
||||||
const { targetInfos } = await cdp.send('Target.getTargets');
|
const { targetInfo } = await s.send('Target.getTargetInfo');
|
||||||
const t = targetInfos.find((x) => x.type === 'page' && x.url.startsWith('http'));
|
const { windowId } = await cdp.send('Browser.getWindowForTarget', { targetId: targetInfo.targetId });
|
||||||
if (!t) return;
|
return windowId;
|
||||||
const { windowId } = await cdp.send('Browser.getWindowForTarget', { targetId: t.targetId });
|
} finally { await s.detach().catch(() => {}); }
|
||||||
await cdp.send('Browser.setWindowBounds', { windowId, bounds: { windowState: fs ? 'fullscreen' : 'normal' } });
|
|
||||||
winFs = fs;
|
|
||||||
} catch { /* best-effort */ }
|
|
||||||
};
|
};
|
||||||
const fsTimer = setInterval(async () => {
|
const fsTimer = setInterval(async () => {
|
||||||
try {
|
try {
|
||||||
let anyFs = false;
|
let fsPage = null;
|
||||||
for (const p of ctx.pages()) {
|
for (const p of ctx.pages()) {
|
||||||
if (await p.evaluate(() => !!document.fullscreenElement).catch(() => false)) { anyFs = true; break; }
|
if (await p.evaluate(() => !!document.fullscreenElement).catch(() => false)) { fsPage = p; break; }
|
||||||
|
}
|
||||||
|
if (fsPage && fsWindowId === null) {
|
||||||
|
const windowId = await windowIdFor(fsPage);
|
||||||
|
await cdp.send('Browser.setWindowBounds', { windowId, bounds: { windowState: 'fullscreen' } });
|
||||||
|
fsWindowId = windowId;
|
||||||
|
} else if (!fsPage && fsWindowId !== null) {
|
||||||
|
await cdp.send('Browser.setWindowBounds', { windowId: fsWindowId, bounds: { windowState: 'normal' } });
|
||||||
|
fsWindowId = null;
|
||||||
}
|
}
|
||||||
if (anyFs && !winFs) await setWindowFs(true);
|
|
||||||
else if (!anyFs && winFs) await setWindowFs(false);
|
|
||||||
} catch { /* best-effort */ }
|
} catch { /* best-effort */ }
|
||||||
}, 600);
|
}, 600);
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const config = {
|
|||||||
streamHw: (process.env.STREAM_HW ?? "1") !== "0",
|
streamHw: (process.env.STREAM_HW ?? "1") !== "0",
|
||||||
streamAudio: (process.env.STREAM_AUDIO ?? "1") !== "0",
|
streamAudio: (process.env.STREAM_AUDIO ?? "1") !== "0",
|
||||||
streamAudioSource: process.env.STREAM_AUDIO_SOURCE ?? "@DEFAULT_MONITOR@",
|
streamAudioSource: process.env.STREAM_AUDIO_SOURCE ?? "@DEFAULT_MONITOR@",
|
||||||
|
screenBrowser: (process.env.STREAM_BROWSER ?? "true") !== "false",
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
const guildId = process.env.DISCORD_GUILD_ID;
|
const guildId = process.env.DISCORD_GUILD_ID;
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export class SelfbotStreamer implements ScreenStreamer {
|
|||||||
private streamer: any = null;
|
private streamer: any = null;
|
||||||
private capture: ChildProcess | null = null;
|
private capture: ChildProcess | null = null;
|
||||||
private keepalive: VncKeepalive | null = null;
|
private keepalive: VncKeepalive | null = null;
|
||||||
|
private helper: ChildProcess | null = null;
|
||||||
private controller: AbortController | null = null;
|
private controller: AbortController | null = null;
|
||||||
private active = false;
|
private active = false;
|
||||||
|
|
||||||
@@ -79,6 +80,12 @@ export class SelfbotStreamer implements ScreenStreamer {
|
|||||||
|
|
||||||
async start(ctx: StreamContext): Promise<string> {
|
async start(ctx: StreamContext): Promise<string> {
|
||||||
if (this.active) return "이미 송출 중입니다.";
|
if (this.active) return "이미 송출 중입니다.";
|
||||||
|
// Screen-share gate: STREAM_BROWSER=false means voice + API/MCP only, so we
|
||||||
|
// never go Live. Enforced HERE (not just in the slash command) so every
|
||||||
|
// caller - including stream-hold.ts - respects it.
|
||||||
|
if (this.config.screenBrowser === false) {
|
||||||
|
return "화면 공유(브라우저) 모드가 꺼져 있습니다 (STREAM_BROWSER=false). 음성 + API/MCP 모드로만 동작합니다.";
|
||||||
|
}
|
||||||
if (!this.config.selfbotToken) {
|
if (!this.config.selfbotToken) {
|
||||||
return "DISCORD_SELFBOT_TOKEN이 설정되지 않았습니다 (.env). 버너 계정 토큰을 넣어주세요.";
|
return "DISCORD_SELFBOT_TOKEN이 설정되지 않았습니다 (.env). 버너 계정 토큰을 넣어주세요.";
|
||||||
}
|
}
|
||||||
@@ -97,6 +104,7 @@ export class SelfbotStreamer implements ScreenStreamer {
|
|||||||
let streamer: any = null;
|
let streamer: any = null;
|
||||||
let capture: ChildProcess | null = null;
|
let capture: ChildProcess | null = null;
|
||||||
let keepalive: VncKeepalive | null = null;
|
let keepalive: VncKeepalive | null = null;
|
||||||
|
let helper: ChildProcess | null = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { selfbot, vs } = await this.loadLib();
|
const { selfbot, vs } = await this.loadLib();
|
||||||
@@ -191,6 +199,23 @@ export class SelfbotStreamer implements ScreenStreamer {
|
|||||||
keepalive.start();
|
keepalive.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Browser-side broadcast defaults (ad-skip, subtitle rule, fullscreen
|
||||||
|
// toolbar hiding) run in a small CDP helper tied to the stream lifecycle.
|
||||||
|
// It attaches to the on-screen Chrome (CDP_PORT) and fail-opens if Chrome
|
||||||
|
// or its deps are absent, so it can never break the stream.
|
||||||
|
try {
|
||||||
|
const helperPath = new URL("../../scripts/stream-test/broadcast-helper.mjs", import.meta.url).pathname;
|
||||||
|
helper = this.helper = spawn("node", [helperPath], {
|
||||||
|
env: { ...process.env, CDP_PORT: process.env.CDP_PORT ?? "9222" },
|
||||||
|
stdio: "ignore",
|
||||||
|
});
|
||||||
|
helper.on("error", () => {
|
||||||
|
/* node/playwright missing: the stream runs without the helper */
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
|
||||||
const { command, output } = prepareStream(
|
const { command, output } = prepareStream(
|
||||||
capture.stdout,
|
capture.stdout,
|
||||||
{
|
{
|
||||||
@@ -234,6 +259,11 @@ export class SelfbotStreamer implements ScreenStreamer {
|
|||||||
} catch {
|
} catch {
|
||||||
/* ignore */
|
/* ignore */
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
helper?.kill();
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
streamer?.leaveVoice?.();
|
streamer?.leaveVoice?.();
|
||||||
streamer?.client?.destroy?.();
|
streamer?.client?.destroy?.();
|
||||||
@@ -242,6 +272,7 @@ export class SelfbotStreamer implements ScreenStreamer {
|
|||||||
}
|
}
|
||||||
if (this.capture === capture) this.capture = null;
|
if (this.capture === capture) this.capture = null;
|
||||||
if (this.keepalive === keepalive) this.keepalive = null;
|
if (this.keepalive === keepalive) this.keepalive = null;
|
||||||
|
if (this.helper === helper) this.helper = null;
|
||||||
if (this.streamer === streamer) this.streamer = null;
|
if (this.streamer === streamer) this.streamer = null;
|
||||||
this.controller = null;
|
this.controller = null;
|
||||||
this.active = false;
|
this.active = false;
|
||||||
@@ -262,6 +293,11 @@ export class SelfbotStreamer implements ScreenStreamer {
|
|||||||
} catch {
|
} catch {
|
||||||
/* ignore */
|
/* ignore */
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
helper?.kill();
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
streamer?.leaveVoice?.();
|
streamer?.leaveVoice?.();
|
||||||
streamer?.client?.destroy?.();
|
streamer?.client?.destroy?.();
|
||||||
@@ -275,6 +311,7 @@ export class SelfbotStreamer implements ScreenStreamer {
|
|||||||
if (this.controller === controller) {
|
if (this.controller === controller) {
|
||||||
if (this.capture === capture) this.capture = null;
|
if (this.capture === capture) this.capture = null;
|
||||||
if (this.keepalive === keepalive) this.keepalive = null;
|
if (this.keepalive === keepalive) this.keepalive = null;
|
||||||
|
if (this.helper === helper) this.helper = null;
|
||||||
if (this.streamer === streamer) this.streamer = null;
|
if (this.streamer === streamer) this.streamer = null;
|
||||||
this.controller = null;
|
this.controller = null;
|
||||||
this.active = false;
|
this.active = false;
|
||||||
@@ -299,6 +336,12 @@ export class SelfbotStreamer implements ScreenStreamer {
|
|||||||
/* ignore */
|
/* ignore */
|
||||||
}
|
}
|
||||||
this.keepalive = null;
|
this.keepalive = null;
|
||||||
|
try {
|
||||||
|
this.helper?.kill();
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
this.helper = null;
|
||||||
try {
|
try {
|
||||||
this.streamer?.leaveVoice?.();
|
this.streamer?.leaveVoice?.();
|
||||||
this.streamer?.client?.destroy?.();
|
this.streamer?.client?.destroy?.();
|
||||||
|
|||||||
Reference in New Issue
Block a user