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