feat(stream-test): persistent YouTube ad auto-skipper for the broadcast
Adds ad-skip.mjs: connects over CDP and injects a watcher into every tab (current and future) that clicks "Skip ad" the moment it appears, closes overlay ads, and fast-forwards unskippable ads (seek-to-end + 16x + mute) so they clear in ~1s. Self-contained (no extension, no hosts/network changes) and reconnects across Chrome restarts. Documented in the README. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -21,7 +21,13 @@ real browsing session captured from the X display.
|
|||||||
- `scenario.mjs` - the browse scenario (YouTube -> IU live -> 1080p ->
|
- `scenario.mjs` - the browse scenario (YouTube -> IU live -> 1080p ->
|
||||||
fullscreen -> Naver -> 나무위키), driven with the human helpers. Connects to a
|
fullscreen -> Naver -> 나무위키), driven with the human helpers. Connects to a
|
||||||
Chrome already running with `--remote-debugging-port` (`CDP_PORT`, default
|
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
|
## Run
|
||||||
```
|
```
|
||||||
@@ -30,6 +36,9 @@ bun bot/scripts/stream-test/stream-hold.ts
|
|||||||
|
|
||||||
# Chrome on the streamed display with remote debugging, then:
|
# Chrome on the streamed display with remote debugging, then:
|
||||||
node bot/scripts/stream-test/scenario.mjs
|
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?"
|
Recommended Chrome flags on the streamed display (avoids the "restore pages?"
|
||||||
|
|||||||
58
bot/scripts/stream-test/ad-skip.mjs
Normal file
58
bot/scripts/stream-test/ad-skip.mjs
Normal file
@@ -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));
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user