diff --git a/bot/scripts/stream-test/README.md b/bot/scripts/stream-test/README.md index e41dbf5..36b3572 100644 --- a/bot/scripts/stream-test/README.md +++ b/bot/scripts/stream-test/README.md @@ -21,7 +21,13 @@ real browsing session captured from the X display. - `scenario.mjs` - the browse scenario (YouTube -> IU live -> 1080p -> fullscreen -> Naver -> 나무위키), driven with the human helpers. Connects to a Chrome already running with `--remote-debugging-port` (`CDP_PORT`, default - 9222) on the streamed display. + 9222) on the streamed display. Captions default OFF, auto-enabling a Korean + track when one exists. +- `ad-skip.mjs` - persistent YouTube ad auto-skipper. Connects over CDP and + injects a watcher into every tab (current and future) that clicks "Skip ad" + the instant it appears, closes overlay ads, and fast-forwards unskippable ads + (seek-to-end + 16x + mute). Run it alongside the broadcast. Reconnects across + Chrome restarts. ## Run ``` @@ -30,6 +36,9 @@ bun bot/scripts/stream-test/stream-hold.ts # Chrome on the streamed display with remote debugging, then: node bot/scripts/stream-test/scenario.mjs + +# keep YouTube ads auto-skipped for the whole broadcast (separate process): +node bot/scripts/stream-test/ad-skip.mjs ``` Recommended Chrome flags on the streamed display (avoids the "restore pages?" diff --git a/bot/scripts/stream-test/ad-skip.mjs b/bot/scripts/stream-test/ad-skip.mjs new file mode 100644 index 0000000..6ff4e71 --- /dev/null +++ b/bot/scripts/stream-test/ad-skip.mjs @@ -0,0 +1,58 @@ +// Persistent YouTube ad auto-skipper for the broadcast Chrome. Connects over +// CDP and injects a small watcher into every tab (current and future). The +// watcher clicks the "Skip ad" button the instant it appears, closes overlay +// ads, and fast-forwards unskippable ads (seek to end + 16x + mute) so they are +// gone in ~1s. Self-contained: no extension, no network/hosts changes. +// +// node bot/scripts/stream-test/ad-skip.mjs (CDP_PORT, default 9222) +import { chromium } from 'playwright'; + +const CDP = process.env.CDP_PORT || '9222'; + +const WATCH = `(() => { + if (window.__ytAdSkip) return; window.__ytAdSkip = true; + const tick = () => { + try { + const p = document.getElementById('movie_player'); + const adShowing = !!(p && p.classList && p.classList.contains('ad-showing')); + // 1) click any skip button + const skip = document.querySelector( + '.ytp-ad-skip-button, .ytp-ad-skip-button-modern, .ytp-skip-ad-button, .ytp-ad-skip-button-container button'); + if (skip) skip.click(); + // 2) close overlay/banner ads + document.querySelectorAll('.ytp-ad-overlay-close-button, .ytp-ad-overlay-close-container button').forEach((b) => b.click()); + // 3) fast-forward an unskippable ad + const v = document.querySelector('video'); + if (v) { + if (adShowing) { + v.muted = true; + if (isFinite(v.duration) && v.duration > 0) { try { v.currentTime = v.duration; } catch {} } + v.playbackRate = 16; + } else if (v.playbackRate > 2) { + v.playbackRate = 1; // restore after the ad + } + } + } catch {} + }; + setInterval(tick, 250); +})();`; + +async function arm(page) { + try { await page.addInitScript(WATCH); } catch {} // survives navigations + try { await page.evaluate(WATCH); } catch {} // arm the already-loaded doc +} + +async function session() { + const b = await chromium.connectOverCDP(`http://localhost:${CDP}`); + const ctx = b.contexts()[0]; + for (const p of ctx.pages()) await arm(p); + ctx.on('page', arm); // new tabs + console.log('ad-skip armed on', ctx.pages().length, 'tab(s)'); + await new Promise((resolve) => b.on('disconnected', resolve)); // until Chrome goes away +} + +// Reconnect across Chrome restarts so the broadcast stays ad-free. +while (true) { + try { await session(); } catch (e) { /* CDP down */ } + await new Promise((r) => setTimeout(r, 3000)); +}