Feat : page streamer
Some checks failed
release / build (push) Successful in 27s
release / verify-windows (push) Failing after 1m3s

This commit is contained in:
jeanotx32
2026-08-11 20:04:08 +02:00
parent da9025fbf1
commit 4f7b8f56ae
12 changed files with 698 additions and 330 deletions

View File

@@ -77,8 +77,53 @@ requête initiée par la page
└──► lecture normale, aucune requête supplémentaire vers la plateforme
```
Elle n'observe que `*.doppiocdn.com` (le CDN média), n'envoie rien ailleurs qu'en
`127.0.0.1`, et n'a aucune permission de lecture de page ni de cookies.
### Sur la portée des permissions
Le manifeste demande `<all_urls>`, et c'est délibéré : **une requête vers un
domaine non déclaré est purement invisible** pour une extension. Or les noms
d'hôte du CDN n'ont été observés que sur le leurre anonyme — rien ne garantit
qu'une session authentifiée soit servie par les mêmes. Restreindre la permission
aux hôtes connus risquait de ne rien capturer du tout, sans le moindre indice.
Le filtrage réel est dans le code, pas dans la permission :
- seules les URL finissant par `.m3u8`, `.m4s`, `.mp4` ou `.ts` sont lues ;
- tout le reste retourne immédiatement, sans être ouvert ;
- la seule destination sortante est `127.0.0.1:8099` ;
- aucune permission `tabs`, `cookies`, `storage` ni `scripting`.
L'extension journalise les hôtes qui servent du média. Une fois le bon CDN connu,
la permission peut être resserrée sur lui.
## Dépannage
### « Ne peut pas lire les données de ce site »
C'est l'indicateur de permissions de Firefox, et il parle du **site affiché dans
l'onglet**. La version 0.1.0 ne déclarait que le CDN, pas `stripchat.com` : sur cet
onglet, Firefox annonçait donc à juste titre n'avoir aucun droit de lecture.
Corrigé en 0.2.0 — recharge le module temporaire.
### Vérifier que la capture fonctionne vraiment
L'indicateur de permissions ne dit rien de l'activité réelle. Deux signaux fiables :
1. **Le compteur sur l'icône** de l'extension s'incrémente. En rouge, le récepteur
ne répond pas (`node receiver.mjs` non lancé).
2. **La console de l'extension** : `about:debugging#/runtime/this-firefox`
*Inspecter* en face de Stream Capture. Elle liste les hôtes qui servent du
média, et résume toutes les 30 s :
```
stream-capture : média servi par media-hls.doppiocdn.com
stream-capture : 47 copiés, 0 en échec — hôtes : media-hls.doppiocdn.com (47)
```
### Aucun hôte média détecté
Si la console ne mentionne aucun hôte alors que la vidéo joue, le lecteur ne passe
pas par des requêtes HTTP filtrables — par exemple du WebRTC, qui ne transite pas
par `webRequest`. Recopie-moi la sortie de la console : c'est l'information qui
manque pour trancher.
## Limites de ce banc d'essai

View File

@@ -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.',
);

View File

@@ -1,8 +1,8 @@
{
"manifest_version": 2,
"name": "Stream Capture (test)",
"version": "0.1.0",
"description": "Copie les segments média que Firefox télécharge déjà, vers un récepteur local. Banc d'essai pour Stream Control.",
"version": "0.2.0",
"description": "Copie les segments média que Firefox télécharge déjà, vers un récepteur local (127.0.0.1). Banc d'essai pour Stream Control.",
"browser_specific_settings": {
"gecko": {
@@ -11,12 +11,7 @@
}
},
"permissions": [
"webRequest",
"webRequestBlocking",
"*://*.doppiocdn.com/*",
"http://127.0.0.1/*"
],
"permissions": ["webRequest", "webRequestBlocking", "<all_urls>"],
"background": {
"scripts": ["background.js"]