153 lines
5.7 KiB
JavaScript
153 lines
5.7 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Récepteur local des segments copiés par l'extension Firefox.
|
|
*
|
|
* node receiver.mjs [--dir ./captures] [--port 8099]
|
|
*
|
|
* Écrit chaque segment dans un dossier de session, puis à l'arrêt (Ctrl-C)
|
|
* assemble le tout et rend son verdict : les octets reçus sont-ils du média
|
|
* lisible, ou faut-il une transformation côté lecteur ?
|
|
*
|
|
* C'est ce verdict qui décide si la capture directe est exploitable.
|
|
*/
|
|
|
|
import { execFile } from 'node:child_process';
|
|
import fs from 'node:fs';
|
|
import http from 'node:http';
|
|
import path from 'node:path';
|
|
import { promisify } from 'node:util';
|
|
|
|
const run = promisify(execFile);
|
|
|
|
const args = process.argv.slice(2);
|
|
const argOf = (name, fallback) => {
|
|
const index = args.indexOf(name);
|
|
return index !== -1 && args[index + 1] ? args[index + 1] : fallback;
|
|
};
|
|
|
|
const PORT = Number(argOf('--port', '8099'));
|
|
const ROOT = path.resolve(argOf('--dir', './captures'));
|
|
const SESSION = path.join(ROOT, new Date().toISOString().replace(/[:.]/g, '-'));
|
|
|
|
fs.mkdirSync(SESSION, { recursive: true });
|
|
|
|
let index = 0;
|
|
const manifests = [];
|
|
const segments = [];
|
|
let initFile = null;
|
|
|
|
function safeName(url) {
|
|
const base = path.basename(new URL(url).pathname) || 'segment';
|
|
return base.replace(/[^A-Za-z0-9._-]/g, '_').slice(0, 80);
|
|
}
|
|
|
|
const server = http.createServer((req, res) => {
|
|
if (req.method !== 'POST' || !req.url?.startsWith('/ingest')) {
|
|
res.writeHead(404).end();
|
|
return;
|
|
}
|
|
|
|
const sourceUrl = String(req.headers['x-source-url'] ?? '');
|
|
const chunks = [];
|
|
|
|
req.on('data', (chunk) => chunks.push(chunk));
|
|
req.on('end', () => {
|
|
const body = Buffer.concat(chunks);
|
|
const name = safeName(sourceUrl);
|
|
const file = path.join(SESSION, `${String(++index).padStart(4, '0')}_${name}`);
|
|
fs.writeFileSync(file, body);
|
|
|
|
if (/\.m3u8$/i.test(name)) {
|
|
manifests.push({ file, url: sourceUrl, text: body.toString('utf8') });
|
|
process.stdout.write(` manifeste ${name} (${body.length} o)\n`);
|
|
} else if (/init/i.test(name)) {
|
|
initFile = file;
|
|
process.stdout.write(` init ${name} (${body.length} o)\n`);
|
|
} else {
|
|
segments.push(file);
|
|
process.stdout.write(` segment ${name} (${body.length} o)\r`);
|
|
}
|
|
|
|
res.writeHead(204).end();
|
|
});
|
|
});
|
|
|
|
server.listen(PORT, '127.0.0.1', () => {
|
|
console.log(`Récepteur prêt sur http://127.0.0.1:${PORT}/ingest`);
|
|
console.log(`Session : ${SESSION}`);
|
|
console.log('\nOuvre le stream dans Firefox, puis Ctrl-C ici pour le verdict.\n');
|
|
});
|
|
|
|
// --- Verdict ----------------------------------------------------------------
|
|
|
|
async function verdict() {
|
|
console.log('\n\n─── Analyse ───────────────────────────────────────────────\n');
|
|
console.log(` manifestes : ${manifests.length}`);
|
|
console.log(` segments : ${segments.length}${initFile ? ' (+ init)' : ' (aucun init)'}`);
|
|
|
|
if (!manifests.length && !segments.length) {
|
|
console.log('\n Rien n\'a été capturé. Vérifie que l\'extension est chargée et');
|
|
console.log(' que la lecture a bien démarré dans l\'onglet.\n');
|
|
return;
|
|
}
|
|
|
|
// 1. Le manifeste reçu est-il le direct, ou le leurre publicitaire ?
|
|
const live = manifests.find((m) => !/\/cpa\//.test(m.text) && /#EXTINF/.test(m.text));
|
|
if (manifests.length) {
|
|
const decoy = manifests.some((m) => /\/cpa\//.test(m.text));
|
|
console.log(`\n Manifeste réel : ${live ? 'oui' : 'non'}`);
|
|
console.log(` Leurre publicitaire détecté : ${decoy ? 'oui' : 'non'}`);
|
|
const mouflon = manifests.some((m) => /MOUFLON/.test(m.text));
|
|
console.log(` Tags MOUFLON : ${mouflon ? 'présents' : 'absents'}`);
|
|
}
|
|
|
|
if (!segments.length) {
|
|
console.log('\n Aucun segment média capturé : rien à assembler.\n');
|
|
return;
|
|
}
|
|
|
|
// 2. Les octets reçus forment-ils un média lisible ?
|
|
const output = path.join(SESSION, 'assemble.mp4');
|
|
const parts = initFile ? [initFile, ...segments] : segments;
|
|
fs.writeFileSync(output, Buffer.concat(parts.map((file) => fs.readFileSync(file))));
|
|
console.log(`\n Assemblé : ${output} (${(fs.statSync(output).size / 1e6).toFixed(1)} Mo)`);
|
|
|
|
try {
|
|
const probe = await run(
|
|
'ffprobe',
|
|
['-hide_banner', '-v', 'error', '-show_entries',
|
|
'format=format_name,duration:stream=codec_type,codec_name,width,height',
|
|
'-of', 'default=noprint_wrappers=1', output],
|
|
{ timeout: 30_000 },
|
|
).catch((err) => {
|
|
if (err.code === 'ENOENT') throw err; // ffprobe absent : traité plus bas
|
|
return { stdout: '', stderr: err.stderr ?? String(err) };
|
|
});
|
|
|
|
const { stdout, stderr } = probe;
|
|
|
|
if (stdout.trim()) {
|
|
console.log('\n ✓ ffprobe lit le fichier — les segments sont du média exploitable :\n');
|
|
console.log(stdout.split('\n').map((l) => ` ${l}`).join('\n'));
|
|
console.log('\n → La capture directe est viable. On peut l\'intégrer à l\'application.\n');
|
|
} else {
|
|
console.log('\n ✗ ffprobe ne reconnaît pas le fichier assemblé.');
|
|
if (stderr) console.log(` ${stderr.trim().split('\n')[0]}`);
|
|
console.log('\n → Deux causes possibles : assemblage incorrect (init manquant), ou');
|
|
console.log(' segments transformés côté lecteur. Envoie-moi cette sortie.\n');
|
|
}
|
|
} catch (err) {
|
|
console.log(`\n ffprobe indisponible (${err.code === 'ENOENT' ? 'non installé' : err.message}).`);
|
|
console.log(' Installe-le : sudo apt install ffmpeg — puis relance l\'analyse avec :');
|
|
console.log(` ffprobe "${output}"\n`);
|
|
}
|
|
}
|
|
|
|
let closing = false;
|
|
process.on('SIGINT', () => {
|
|
if (closing) process.exit(1);
|
|
closing = true;
|
|
server.close();
|
|
void verdict().then(() => process.exit(0));
|
|
});
|