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:
javis-bot
2026-06-10 16:09:06 +09:00
parent e154404baf
commit 0241628fed

View File

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