fix(stream-test): restore audio after ads, enforce subtitle rule broadcast-wide, commit the 60fps MV path
Addresses review of the ad/subtitle work: - ad mute leak: the ad-skipper muted during an ad but never un-muted, so the main video stayed silent after the first ad. Save the pre-ad muted/playbackRate and restore them when the ad ends (verified: muted false -> true -> false). - captions were only applied once when scenario.mjs ran, not for the whole broadcast. Move the rule (OFF by default, Korean ON if offered) into the persistent helper so it runs per video, and ENFORCE it every tick - one-shot did not hold because YouTube silently re-enabled captions (verified it now stays off across 8s). - merge ad-skip.mjs + captions into broadcast-helper.mjs (one CDP process). - the actual 60fps MV test now lives in the repo: scenario.mjs gains MV_QUERY (search + auto-pick the first >=60fps result) and WATCH_SECONDS, with the fullscreen-toolbar-hide fix. The broadcast runs via the committed stream-hold.ts (audio + keepalive), not an out-of-repo copy. - document the test env vars (CDP_PORT, HOLD_MS, TEST_*, MV_QUERY, WATCH_SECONDS) in .env.example. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,58 +0,0 @@
|
||||
// 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