Feat : page streamer
This commit is contained in:
@@ -6,43 +6,68 @@
|
||||
* vers la page : le lecteur continue de fonctionner normalement, et l'extension
|
||||
* n'émet aucune requête vers la plateforme.
|
||||
*
|
||||
* C'est la différence avec un script qui irait rechercher le flux : ici on ne
|
||||
* fait que conserver ce que la session authentifiée a légitimement reçu.
|
||||
* Portée des permissions : le manifeste demande <all_urls> parce qu'une requête
|
||||
* vers un domaine non autorisé est purement invisible pour une extension — et on
|
||||
* ignore quels hôtes servent le flux d'une session authentifiée. Le filtrage
|
||||
* réel est ici : seules les URL de média sont lues, et rien n'est transmis
|
||||
* ailleurs qu'à 127.0.0.1.
|
||||
*/
|
||||
|
||||
const RECEIVER = 'http://127.0.0.1:8099/ingest';
|
||||
|
||||
// Playlists et segments servis par le CDN média de la plateforme.
|
||||
/** Playlists et segments. Tout le reste est ignoré sans être lu. */
|
||||
const MEDIA_PATTERN = /\.(m3u8|m4s|mp4|ts)(\?|$)/i;
|
||||
|
||||
let copied = 0;
|
||||
let failed = 0;
|
||||
let receiverWarned = false;
|
||||
|
||||
/** Hôtes ayant servi du média : sert à identifier le CDN réellement utilisé. */
|
||||
const seenHosts = new Map();
|
||||
|
||||
function updateBadge() {
|
||||
const text = failed ? `${copied}/${failed}!` : String(copied);
|
||||
const text = failed ? `${copied}/${failed}` : String(copied);
|
||||
browser.browserAction.setBadgeText({ text: copied || failed ? text : '' });
|
||||
browser.browserAction.setBadgeBackgroundColor({ color: failed ? '#b91c1c' : '#2563eb' });
|
||||
}
|
||||
|
||||
function noteHost(url) {
|
||||
try {
|
||||
const { host } = new URL(url);
|
||||
const count = (seenHosts.get(host) ?? 0) + 1;
|
||||
seenHosts.set(host, count);
|
||||
if (count === 1) {
|
||||
console.log(`stream-capture : média servi par ${host}`);
|
||||
}
|
||||
} catch {
|
||||
/* URL exotique : sans importance pour le diagnostic */
|
||||
}
|
||||
}
|
||||
|
||||
async function forward(url, chunks) {
|
||||
try {
|
||||
const blob = new Blob(chunks);
|
||||
const body = await blob.arrayBuffer();
|
||||
|
||||
await fetch(RECEIVER, {
|
||||
const response = await fetch(RECEIVER, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/octet-stream',
|
||||
'x-source-url': url,
|
||||
},
|
||||
headers: { 'content-type': 'application/octet-stream', 'x-source-url': url },
|
||||
body,
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error(`récepteur : HTTP ${response.status}`);
|
||||
copied += 1;
|
||||
} catch (err) {
|
||||
// Récepteur non lancé : on n'interrompt surtout pas la lecture pour autant.
|
||||
failed += 1;
|
||||
console.warn('stream-capture : envoi impossible', err);
|
||||
// Récepteur non lancé : on n'interrompt surtout pas la lecture pour autant.
|
||||
if (!receiverWarned) {
|
||||
receiverWarned = true;
|
||||
console.error(
|
||||
`stream-capture : le récepteur ne répond pas sur ${RECEIVER} — ` +
|
||||
'lance « node receiver.mjs » sur cette machine.',
|
||||
err,
|
||||
);
|
||||
}
|
||||
}
|
||||
updateBadge();
|
||||
}
|
||||
@@ -51,7 +76,17 @@ browser.webRequest.onBeforeRequest.addListener(
|
||||
(details) => {
|
||||
if (!MEDIA_PATTERN.test(details.url)) return {};
|
||||
|
||||
const filter = browser.webRequest.filterResponseData(details.requestId);
|
||||
noteHost(details.url);
|
||||
|
||||
let filter;
|
||||
try {
|
||||
filter = browser.webRequest.filterResponseData(details.requestId);
|
||||
} catch (err) {
|
||||
// Requête non filtrable (cache, redirection…) : la lecture continue.
|
||||
console.warn('stream-capture : filtrage impossible pour', details.url, err);
|
||||
return {};
|
||||
}
|
||||
|
||||
const chunks = [];
|
||||
|
||||
filter.ondata = (event) => {
|
||||
@@ -62,18 +97,30 @@ browser.webRequest.onBeforeRequest.addListener(
|
||||
|
||||
filter.onstop = () => {
|
||||
filter.disconnect();
|
||||
void forward(details.url, chunks);
|
||||
if (chunks.length) void forward(details.url, chunks);
|
||||
};
|
||||
|
||||
filter.onerror = () => {
|
||||
// Le filtre a lâché : la requête suit son cours normalement.
|
||||
console.warn('stream-capture : filtre interrompu', filter.error);
|
||||
console.warn('stream-capture : filtre interrompu —', filter.error);
|
||||
};
|
||||
|
||||
return {};
|
||||
},
|
||||
{ urls: ['*://*.doppiocdn.com/*'] },
|
||||
{ urls: ['<all_urls>'] },
|
||||
['blocking'],
|
||||
);
|
||||
|
||||
console.log('stream-capture : actif, récepteur attendu sur', RECEIVER);
|
||||
/** Résumé périodique, à recopier en cas de souci. */
|
||||
setInterval(() => {
|
||||
if (!seenHosts.size) return;
|
||||
const summary = [...seenHosts.entries()]
|
||||
.map(([host, count]) => `${host} (${count})`)
|
||||
.join(', ');
|
||||
console.log(`stream-capture : ${copied} copiés, ${failed} en échec — hôtes : ${summary}`);
|
||||
}, 30_000);
|
||||
|
||||
console.log(
|
||||
'stream-capture actif. Récepteur attendu sur ' +
|
||||
RECEIVER +
|
||||
'. Aucun hôte média détecté pour l\'instant.',
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user