156 lines
8.7 KiB
JavaScript
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; });
|