Files
chopin/tests/dev/stream.e2e.js
T

156 lines
8.7 KiB
JavaScript

// Test de bout en bout : vrai yt-dlp, vrai YouTube, sans proxy (WARP non démarré => route directe)
// Usage : node stream.e2e.js [--no-update] [--no-cookies]
const fs = require('fs');
const path = require('path');
const { execFileSync } = require('child_process');
const { BACKEND, __glob, isolateData, check, done, req, chopinTmpDirs } = require('./_setup');
const dataDir = isolateData('e2e');
const youtube = req('src/youtube');
const { getStream, activeProcesses } = req('src/player/Method/Youtube.js');
const warp = req('src/utils/WarpProxy');
const { cookies, settings, ytdlp, diagnostics } = youtube;
const args = process.argv.slice(2);
const TARGET = 200 * 1024;
const LONG_URL = 'https://www.youtube.com/watch?v=dQw4w9WgXcQ';
function procCount(image) {
if (process.platform !== 'win32') return null;
try {
const out = execFileSync('tasklist', ['/FI', `IMAGENAME eq ${image}`, '/NH'], { encoding: 'utf8', windowsHide: true });
return out.split(/\r?\n/).filter(l => l.toLowerCase().startsWith(image.toLowerCase())).length;
} catch (e) { return null; }
}
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
/**
* Lit ~200 Ko puis annule via AbortController
*/
async function readThenAbort(label, seek, { expectRaw = false, url } = {}) {
console.log(`\n# ${label}`);
const controller = new AbortController();
const progress = [];
const t0 = Date.now();
const song = { id: 'e2e', title: 'e2e', url: url || settings.get().testUrl, type: 'youtube' };
const stream = await getStream(song, seek, { signal: controller.signal, guildId: 'e2e-guild', onProgress: (p) => progress.push(p) });
const tStream = Date.now() - t0;
console.log(` getStream -> ${stream ? 'flux' : 'null'} en ${tStream} ms, progression : ${JSON.stringify(progress)}`);
console.log(` activeProcesses pendant la lecture : ${JSON.stringify([...activeProcesses.entries()].map(([g, p]) => ({ g, route: p.route, identity: p.identity, pid: !!p.pid })))}`);
if (!stream) {
console.log(' diagnostics :', JSON.stringify(diagnostics.getRecent(4), null, 0));
return { ok: false };
}
check(`${label} : streamType ${expectRaw ? '"raw"' : 'conteneur'}`, expectRaw ? stream.streamType === 'raw' : stream.streamType === undefined);
const chunks = [];
let total = 0;
await new Promise((resolve) => {
const timer = setTimeout(resolve, 60000);
stream.on('data', (c) => {
if (total >= TARGET) return;
chunks.push(c);
total += c.length;
if (total >= TARGET) { clearTimeout(timer); resolve(); }
});
stream.on('close', () => { clearTimeout(timer); resolve(); });
stream.on('error', () => {});
});
const buf = Buffer.concat(chunks);
console.log(` ${total} octets lus en ${Date.now() - t0} ms`);
check(`${label} : ~200 Ko reçus`, total >= TARGET, total);
if (expectRaw) {
// PCM s16le stéréo : vérifie qu'il y a du signal (pas que des zéros)
let sumSq = 0, n = 0, nonZero = 0;
for (let i = 0; i + 1 < buf.length; i += 2) {
const v = buf.readInt16LE(i);
sumSq += v * v; n++;
if (v !== 0) nonZero++;
}
const rms = Math.sqrt(sumSq / Math.max(1, n));
console.log(` PCM : ${n} échantillons, RMS ${rms.toFixed(1)}, ${(100 * nonZero / Math.max(1, n)).toFixed(1)} % non nuls, ~${(n / 2 / 48000).toFixed(2)} s d'audio`);
check(`${label} : PCM non silencieux`, rms > 10 && nonZero > n / 10);
} else {
// WebM (EBML 1A45DFA3) ou MP4 (ftyp)
const magic = buf.subarray(0, 4).toString('hex');
const isWebm = magic === '1a45dfa3';
const isMp4 = buf.subarray(4, 8).toString() === 'ftyp';
console.log(` conteneur : ${isWebm ? 'WebM' : isMp4 ? 'MP4' : 'inconnu (' + magic + ')'}`);
check(`${label} : conteneur audio reconnu`, isWebm || isMp4);
}
controller.abort();
// Processus tués et copies de cookies supprimées
let waited = 0;
while ((ytdlp.activeCount() > 0 || activeProcesses.size > 0 || cookies.liveCopyCount() > 0) && waited < 5000) {
await sleep(100); waited += 100;
}
console.log(` après abort (${waited} ms) : processus suivis ${ytdlp.activeCount()}, activeProcesses ${activeProcesses.size}, copies de cookies ${cookies.liveCopyCount()}, stream détruit ${stream.destroyed}`);
check(`${label} : tous les processus terminés`, ytdlp.activeCount() === 0 && activeProcesses.size === 0);
check(`${label} : flux détruit`, stream.destroyed === true);
check(`${label} : aucune copie chopin-ytc-* restante`, chopinTmpDirs().length === 0 && cookies.liveCopyCount() === 0, chopinTmpDirs());
return { ok: true };
}
(async () => {
console.log('routes WARP :', JSON.stringify(warp.getRoutes().map(r => r ? 'warp' : 'direct')));
const bin = ytdlp.resolveBinary();
const ff = ytdlp.resolveFfmpeg();
console.log(`yt-dlp : ${bin.source} ${path.relative(BACKEND, bin.path) || bin.path}`);
console.log(`ffmpeg : ${ff?.source} ${ff ? path.relative(BACKEND, ff.path) : '-'}`);
console.log(`version : ${await ytdlp.getVersion({ force: true })}`);
const before = { ytdlp: procCount('yt-dlp.exe'), ffmpeg: procCount('ffmpeg.exe') };
console.log('processus système avant :', JSON.stringify(before));
if (!args.includes('--no-update')) {
console.log('\n# Mise à jour (yt-dlp -U)');
const up = await ytdlp.update({ reason: 'test e2e' });
console.log(' ', JSON.stringify({ ok: up.ok, updatable: up.updatable, before: up.before, after: up.after, message: up.message }));
console.log(' sortie :', up.output.split('\n').slice(-4).join(' | '));
check('update : version connue après', Boolean(up.after));
}
await readThenAbort('Lecture sans seek (anonyme, directe)', 0);
// testUrl (Me at the zoo) ne dure que 19 s : seek 30 s sur une vidéo plus longue
await readThenAbort('Seek 30 s (natif : yt-dlp -> ffmpeg -> PCM brut)', 30, { expectRaw: true, url: LONG_URL });
console.log('\n# Seek au-delà de la fin (testUrl, 19 s) : un seul essai, sans clients de repli');
const n0 = diagnostics.getRecent(100).length;
const past = await getStream({ id: 'e2e', title: 'e2e', url: settings.get().testUrl, type: 'youtube' }, 30, { guildId: 'e2e-guild' });
const tries = diagnostics.getRecent(100).length - n0;
check('seek après la fin : null en 1 tentative (final)', past === null && tries === 1 && diagnostics.getRecent(1)[0].cls === 'EMPTY_DOWNLOAD', { tries, last: diagnostics.getRecent(1)[0] });
await sleep(2500);
check('seek après la fin : processus terminés', ytdlp.activeCount() === 0, ytdlp.activeCount());
if (!args.includes('--no-cookies')) {
// Anciens cookies (fichiers réels en lecture seule) importés dans le dossier isolé, mode "always"
__glob.COOKIES = path.join(BACKEND, 'data', 'cookies.txt');
__glob.COOKIES_JSON = path.join(BACKEND, 'data', 'cookies.json');
const mig = cookies.migrateLegacy();
console.log('\n# Cookies : migration', JSON.stringify(mig), '-> valides :', cookies.isValid());
if (cookies.isValid()) {
settings.update({ cookiesMode: 'always' }, { username: 'e2e' });
await readThenAbort('Lecture avec compte (mode always, copie temporaire des cookies)', 0);
const st = cookies.getStatus();
console.log(' statut cookies après lecture :', JSON.stringify({ valid: st.valid, invalidatedAt: !!st.invalidatedAt, invalidReason: st.invalidReason }));
}
}
console.log('\n# Diagnostics');
for (const e of diagnostics.getRecent(20).reverse()) {
console.log(` ${e.outcome.padEnd(7)} ${e.route}/${e.identity}${e.clients ? '/' + e.clients : ''}${e.seek ? '/' + e.seek : ''} ${e.ms} ms ${e.cls || ''} ${e.message || ''}`);
}
const s = diagnostics.getSummary();
console.log(' résumé :', JSON.stringify({ ok: s.ok, failed: s.failed, aborted: s.aborted, byClass: s.byClass, consecutiveBlocks: s.consecutiveBlocks }));
await sleep(1000);
console.log(' getStatus().activeProcesses :', youtube.getStatus().activeProcesses);
const after = { ytdlp: procCount('yt-dlp.exe'), ffmpeg: procCount('ffmpeg.exe') };
console.log('processus système après :', JSON.stringify(after));
check('aucun yt-dlp.exe / ffmpeg.exe orphelin', after.ytdlp === null || (after.ytdlp <= before.ytdlp && after.ffmpeg <= before.ffmpeg), { before, after });
check('aucune copie chopin-ytc-* (fin)', chopinTmpDirs().length === 0, chopinTmpDirs());
fs.rmSync(dataDir, { recursive: true, force: true });
done();
// Timers unref : le process se termine seul
})().catch((e) => { console.error(e); process.exitCode = 1; });