fix(stream-test): restore audio after ads, enforce subtitle rule broadcast-wide, commit the 60fps MV path
Addresses review of the ad/subtitle work (the ad-skip.mjs -> broadcast-helper.mjs rename's other half; the prior commit only recorded the deletion): - 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. The persistent helper now applies the rule (OFF by default, Korean ON if offered) per video and ENFORCES it every tick - one-shot did not hold because YouTube silently re-enabled captions (verified it stays off across 8s). - ad-skip + captions merged into broadcast-helper.mjs (one CDP process). - the 60fps MV test now lives in the repo: scenario.mjs gains MV_QUERY (search + auto-pick the first >=60fps result) and WATCH_SECONDS, plus 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). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,14 @@ const CDP = process.env.CDP_PORT || '9222';
|
||||
const VID = process.env.TEST_VIDEO_ID || 'X_am71G6Vy4';
|
||||
const SEARCH = process.env.TEST_YT_QUERY || '내손을잡아';
|
||||
const NAVER_Q = process.env.TEST_NAVER_QUERY || '아이유';
|
||||
// MV_QUERY mode: search this query and auto-pick the first result that actually
|
||||
// reports >=60fps (instead of clicking the fixed TEST_VIDEO_ID). WATCH_SECONDS
|
||||
// is how long to watch windowed and fullscreen (default 20).
|
||||
const MV_QUERY = process.env.MV_QUERY || '';
|
||||
const WATCH_MS = parseInt(process.env.WATCH_SECONDS || '20', 10) * 1000;
|
||||
|
||||
// Subtitles (off-by-default, Korean-on) and YouTube ad-skipping are applied
|
||||
// broadcast-wide by broadcast-helper.mjs, not here.
|
||||
|
||||
const b = await chromium.connectOverCDP(`http://localhost:${CDP}`);
|
||||
const ctx = b.contexts()[0];
|
||||
@@ -43,46 +51,57 @@ async function browserFullscreen(on) {
|
||||
} catch { /* best-effort */ }
|
||||
}
|
||||
|
||||
// Subtitles: OFF by default, but turn ON Korean when a Korean track exists.
|
||||
async function applyCaptions() {
|
||||
await read(() => { try { document.getElementById('movie_player')?.loadModule?.('captions'); } catch {} });
|
||||
await sleep(800);
|
||||
return read(() => {
|
||||
const pl = document.getElementById('movie_player');
|
||||
if (!pl || !pl.getOption) return 'no-player';
|
||||
let tracks = [];
|
||||
try { tracks = pl.getOption('captions', 'tracklist') || []; } catch {}
|
||||
const ko = tracks.find((t) => /^ko/i.test(t.languageCode || ''));
|
||||
if (ko) { try { pl.setOption('captions', 'track', { languageCode: ko.languageCode }); } catch {} return 'ko-on'; }
|
||||
try { pl.setOption('captions', 'track', {}); } catch {}
|
||||
try { pl.unloadModule('captions'); } catch {}
|
||||
return 'off';
|
||||
});
|
||||
const fpsNow = () => read(() => {
|
||||
try { const s = document.getElementById('movie_player').getStatsForNerds(); const m = (s.resolution || '').match(/@(\d+)/); return m ? +m[1] : null; } catch { return null; }
|
||||
});
|
||||
async function skipAdsQuick() {
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const ad = page.locator('.ytp-ad-skip-button, .ytp-ad-skip-button-modern, .ytp-skip-ad-button');
|
||||
if (await ad.count().catch(() => 0)) { await humanClick(page, ad.first()); await sleep(1200); } else break;
|
||||
}
|
||||
}
|
||||
|
||||
// 1) open YouTube by typing the URL in the address bar
|
||||
await navigateOmnibox('https://www.youtube.com'); await sleep(3000);
|
||||
|
||||
// 2) really type the search and submit
|
||||
// 2) really type the search and submit (fixed query, or the MV query)
|
||||
await humanClick(page, page.locator('input#search, input[name=search_query]').first());
|
||||
await humanType(SEARCH);
|
||||
await humanType(MV_QUERY || SEARCH);
|
||||
await humanKey('Return');
|
||||
await sleep(3800);
|
||||
|
||||
// 3) click the IU concert result with the real mouse
|
||||
let link = page.locator(`a#video-title[href*="${VID}"], a[href*="${VID}"]`).first();
|
||||
if (!(await link.count().catch(() => 0))) link = page.locator('ytd-video-renderer a#video-title, ytd-rich-item-renderer a#video-title').first();
|
||||
await humanClick(page, link);
|
||||
await sleep(3500);
|
||||
await page.waitForSelector('#movie_player', { timeout: 25000 }); await sleep(2000);
|
||||
for (let i = 0; i < 8; i++) { const ad = page.locator('.ytp-ad-skip-button, .ytp-ad-skip-button-modern, .ytp-skip-ad-button'); if (await ad.count().catch(() => 0)) { await humanClick(page, ad.first()); await sleep(1200); } else break; }
|
||||
|
||||
// 4) if paused, press play with the real mouse
|
||||
if (await read(() => document.querySelector('video')?.paused)) {
|
||||
const big = page.locator('.ytp-large-play-button, .ytp-play-button').first();
|
||||
await humanClick(page, big);
|
||||
// open a result with the real mouse, wait for the player, skip ads, ensure playing
|
||||
async function openAndPlay(link) {
|
||||
await humanClick(page, link);
|
||||
await sleep(3500);
|
||||
await page.waitForSelector('#movie_player', { timeout: 25000 }); await sleep(2000);
|
||||
await skipAdsQuick();
|
||||
if (await read(() => document.querySelector('video')?.paused)) {
|
||||
await humanClick(page, page.locator('.ytp-large-play-button, .ytp-play-button').first());
|
||||
}
|
||||
await sleep(1500);
|
||||
}
|
||||
|
||||
// 3) pick the video: in MV mode auto-pick the first result that really reports
|
||||
// >=60fps; otherwise click the fixed concert clip.
|
||||
if (MV_QUERY) {
|
||||
const resultsUrl = `https://www.youtube.com/results?search_query=${encodeURIComponent(MV_QUERY)}&sp=EgQQARgD`;
|
||||
let picked = false;
|
||||
for (let i = 0; i < 5 && !picked; i++) {
|
||||
const results = page.locator('ytd-video-renderer a#video-title, ytd-rich-item-renderer a#video-title');
|
||||
if (!(await results.nth(i).count().catch(() => 0))) break;
|
||||
await openAndPlay(results.nth(i));
|
||||
const fps = await fpsNow();
|
||||
console.log(`MV candidate ${i} fps=${fps}`);
|
||||
if (fps && fps >= 60) { picked = true; break; }
|
||||
await navigateOmnibox(resultsUrl); await sleep(3000);
|
||||
}
|
||||
if (!picked) console.log('MV: no >=60fps result found, using last opened');
|
||||
} else {
|
||||
let link = page.locator(`a#video-title[href*="${VID}"], a[href*="${VID}"]`).first();
|
||||
if (!(await link.count().catch(() => 0))) link = page.locator('ytd-video-renderer a#video-title, ytd-rich-item-renderer a#video-title').first();
|
||||
await openAndPlay(link);
|
||||
}
|
||||
await sleep(1500);
|
||||
|
||||
// 5) set 1080p through the real settings menu (gear -> 화질 -> 1080p), verify
|
||||
async function setQuality1080() {
|
||||
@@ -101,7 +120,6 @@ async function setQuality1080() {
|
||||
return null;
|
||||
}
|
||||
console.log('QUALITY', await setQuality1080());
|
||||
console.log('CAPTIONS', await applyCaptions());
|
||||
|
||||
// 6) turn off autoplay with a real click if it is on
|
||||
const auto = page.locator('.ytp-autonav-toggle-button');
|
||||
@@ -109,15 +127,15 @@ if ((await auto.count().catch(() => 0)) && (await auto.getAttribute('aria-checke
|
||||
await humanHover(page, playerLoc());
|
||||
await humanClick(page, auto);
|
||||
}
|
||||
console.log('STEP watch-1080-windowed'); await sleep(20000);
|
||||
console.log('STEP watch-1080-windowed'); await sleep(WATCH_MS);
|
||||
|
||||
// 7) fullscreen: hide the browser toolbar (CDP), then the real 'f' key makes the
|
||||
// video fill the now toolbar-free screen (innerHeight 1080). 20s.
|
||||
// video fill the now toolbar-free screen (innerHeight 1080).
|
||||
await browserFullscreen(true); await sleep(800);
|
||||
await humanHover(page, playerLoc());
|
||||
await humanKey('f'); await sleep(1500);
|
||||
if (!(await read(() => !!document.fullscreenElement))) { await humanHover(page, playerLoc()); await humanKey('f'); await sleep(1200); }
|
||||
console.log('STEP fullscreen', await read(() => ({ full: !!document.fullscreenElement, h: window.innerHeight }))); await sleep(20000);
|
||||
console.log('STEP fullscreen', await read(() => ({ full: !!document.fullscreenElement, h: window.innerHeight }))); await sleep(WATCH_MS);
|
||||
|
||||
// 8) exit video fullscreen ('f'), then restore the browser toolbar
|
||||
await humanKey('f'); await sleep(1500);
|
||||
|
||||
Reference in New Issue
Block a user