discovery-media-player 0.1.121 → 0.1.123
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/context/storage.js +62 -10
- package/package.json +1 -1
- package/server/page-visionneuse.js +79 -8
package/context/storage.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
const fs = require("node:fs");
|
|
13
13
|
const fsp = require("node:fs/promises");
|
|
14
14
|
const path = require("node:path");
|
|
15
|
+
const { Readable } = require("node:stream");
|
|
15
16
|
|
|
16
17
|
/** Chemin canonique des objets PUBLICS du Storage Supabase. */
|
|
17
18
|
const PUBLIC_OBJECT_PREFIX = "/storage/v1/object/public/";
|
|
@@ -150,12 +151,17 @@ async function readLocal(cible, range) {
|
|
|
150
151
|
// `fh.read()`, quoi qu'il advienne du chemin entre-temps.
|
|
151
152
|
let fh;
|
|
152
153
|
try { fh = await fsp.open(cible, "r"); } catch { return null; }
|
|
154
|
+
// ⚠️ PLUS DE `finally { close() }` : LE DESCRIPTEUR APPARTIENT DÉSORMAIS AU FLUX. Le fermer ici
|
|
155
|
+
// reviendrait à le fermer AVANT que le relais n'ait lu un octet. `lireDepuis` en prend la
|
|
156
|
+
// propriété : soit il le rend au flux (qui le referme en fin ou à l'annulation, `autoClose`),
|
|
157
|
+
// soit il le referme lui-même sur les chemins qui ne lisent rien (416, 413, erreur).
|
|
153
158
|
try {
|
|
154
159
|
const stat = await fh.stat();
|
|
155
|
-
if (!stat.isFile()) return null;
|
|
160
|
+
if (!stat.isFile()) { await fh.close(); return null; }
|
|
156
161
|
return await lireDepuis(fh, stat, cible, range);
|
|
157
|
-
}
|
|
158
|
-
await fh.close();
|
|
162
|
+
} catch (e) {
|
|
163
|
+
await fh.close().catch(() => {});
|
|
164
|
+
throw e;
|
|
159
165
|
}
|
|
160
166
|
}
|
|
161
167
|
|
|
@@ -168,6 +174,7 @@ async function lireDepuis(fh, stat, cible, range) {
|
|
|
168
174
|
if (m[1]) { debut = Number(m[1]); fin = m[2] ? Math.min(Number(m[2]), total - 1) : total - 1; }
|
|
169
175
|
else { debut = Math.max(0, total - Number(m[2])); } // `bytes=-500` : les 500 derniers
|
|
170
176
|
if (!(debut >= 0 && fin >= debut && debut < total)) {
|
|
177
|
+
await fh.close().catch(() => {});
|
|
171
178
|
return reponse(416, {}, Buffer.alloc(0)); // borne absurde : on le dit
|
|
172
179
|
}
|
|
173
180
|
statut = 206;
|
|
@@ -180,16 +187,61 @@ async function lireDepuis(fh, stat, cible, range) {
|
|
|
180
187
|
// fabrique son contexte à l'import, avant que l'exploitant ait pu poser la variable.
|
|
181
188
|
const plafond = Number(process.env.PLAYER_MAX_RELAY_BYTES || 0) || 60 * 1024 * 1024;
|
|
182
189
|
if (fin - debut + 1 > plafond) {
|
|
190
|
+
await fh.close().catch(() => {});
|
|
183
191
|
return reponse(413, {}, Buffer.alloc(0));
|
|
184
192
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
|
|
193
|
+
|
|
194
|
+
// ⚠️ ON DIFFUSE, ON N'ALLOUE PLUS — et le plafond ne suffisait pas à rendre l'allocation sûre.
|
|
195
|
+
//
|
|
196
|
+
// `Buffer.alloc(fin - debut + 1)` réservait la plage DEMANDÉE en une fois : jusqu'à 60 Mio par
|
|
197
|
+
// requête, sous le plafond, donc parfaitement « autorisé ». Un visiteur qui connaît un lien
|
|
198
|
+
// public envoie vingt requêtes SANS `Range` sur un fichier de 50 Mio : environ 1 Gio d'un coup,
|
|
199
|
+
// avant même les surcoûts de Node. Le plafond bornait la taille d'UNE lecture ; rien ne bornait
|
|
200
|
+
// leur PRODUIT par la concurrence, qui est ce que la mémoire subit. (P1 audit externe.)
|
|
201
|
+
//
|
|
202
|
+
// Le relais distant diffuse déjà (`Readable.fromWeb(r.body)`) et compte les octets qui passent :
|
|
203
|
+
// en rendant un `body`, le chemin local emprunte le MÊME code et hérite de sa contre-pression —
|
|
204
|
+
// la mémoire devient fonction de la taille des morceaux, plus de celle du fichier.
|
|
205
|
+
//
|
|
206
|
+
// `autoClose` referme le descripteur en fin de flux ET à la destruction : si le client raccroche,
|
|
207
|
+
// le relais détruit la source et le descripteur part avec elle. C'est pourquoi personne ne le
|
|
208
|
+
// ferme plus en amont.
|
|
209
|
+
const flux = fh.createReadStream({ start: debut, end: fin, autoClose: true });
|
|
210
|
+
// La taille vient d'un `stat` sur le descripteur OUVERT : elle décrit bien ce qu'on envoie, et
|
|
211
|
+
// permet au relais de refuser avant de lire, à la visionneuse d'afficher une progression.
|
|
212
|
+
const entetes = { "content-type": type, "content-length": String(fin - debut + 1) };
|
|
191
213
|
if (statut === 206) entetes["content-range"] = `bytes ${debut}-${fin}/${total}`;
|
|
192
|
-
return
|
|
214
|
+
return reponseFlux(statut, entetes, flux);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Même forme qu'une réponse `fetch`, mais avec un CORPS LISIBLE : `body` est un flux web, ce que le
|
|
219
|
+
* relais sait consommer. `arrayBuffer()` reste disponible pour un appelant qui ne connaît que lui —
|
|
220
|
+
* il consomme alors le flux, donc il réalloue : c'est le chemin de repli, pas celui qu'on emprunte.
|
|
221
|
+
*/
|
|
222
|
+
function reponseFlux(status, entetes, fluxNode) {
|
|
223
|
+
const h = new Map(Object.entries(entetes));
|
|
224
|
+
const fluxWeb = Readable.toWeb(fluxNode);
|
|
225
|
+
return {
|
|
226
|
+
ok: status < 400,
|
|
227
|
+
status,
|
|
228
|
+
headers: { get: (k) => (h.has(String(k).toLowerCase()) ? h.get(String(k).toLowerCase()) : null) },
|
|
229
|
+
// ⚠️ UNE SEULE SOURCE, DONC UNE SEULE CONSOMMATION. `Readable.toWeb` prend possession du flux
|
|
230
|
+
// Node : itérer celui-ci en parallèle de `body` lirait deux fois la même chose, ou rien. On
|
|
231
|
+
// construit donc le flux web UNE fois et `arrayBuffer()` lit depuis LUI — même sémantique que
|
|
232
|
+
// `fetch`, où un corps ne se consomme qu'une fois.
|
|
233
|
+
body: fluxWeb,
|
|
234
|
+
arrayBuffer: async () => {
|
|
235
|
+
const lecteur = fluxWeb.getReader();
|
|
236
|
+
const morceaux = [];
|
|
237
|
+
for (;;) {
|
|
238
|
+
const { done, value } = await lecteur.read();
|
|
239
|
+
if (done) break;
|
|
240
|
+
morceaux.push(Buffer.from(value));
|
|
241
|
+
}
|
|
242
|
+
return Buffer.concat(morceaux);
|
|
243
|
+
},
|
|
244
|
+
};
|
|
193
245
|
}
|
|
194
246
|
|
|
195
247
|
function reponse(status, entetes, corps) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "discovery-media-player",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.123",
|
|
4
4
|
"description": "Self-hosted document viewer: per-recipient tracked links, reading analytics, live presentation. The core knows nothing about the application hosting it — everything it borrows arrives through an injected context.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pdf-viewer",
|
|
@@ -309,8 +309,23 @@ ${LEGAL_CSS}
|
|
|
309
309
|
get onePage(){return onePage;},
|
|
310
310
|
get soloOffered(){return soloOffered;},set soloOffered(v){soloOffered=!!v;}};
|
|
311
311
|
var pdfDoc=null, zoom=1, firstAspect=1.35, rendered={}, io=null, ioCur=null;
|
|
312
|
+
// ATTENTION : LE LECTEUR GARDAIT TOUTES LES PAGES RENDUES (P1 audit externe).
|
|
313
|
+
//
|
|
314
|
+
// rendered[n] n etait jamais evince, les canvas restaient dans le DOM, et un build() de refit
|
|
315
|
+
// n annulait pas les taches deja lancees. Sur un ecran haute densite un seul canvas pese
|
|
316
|
+
// plusieurs dizaines de mega-octets : un document long parcouru en entier faisait exploser la
|
|
317
|
+
// memoire de l onglet. rendered[n] etait aussi pose AVANT le succes — un echec laissait donc la
|
|
318
|
+
// page definitivement consideree comme rendue, sans nouvelle tentative possible.
|
|
319
|
+
//
|
|
320
|
+
// Note de forme : ce script vit dans un litteral de gabarit — pas d accent grave ici.
|
|
321
|
+
var renderGen=0; // invalide les callbacks d un build() precedent
|
|
322
|
+
var taches={}; // n -> RenderTask, pour pouvoir ANNULER
|
|
323
|
+
var enCours={}; // n -> 1 tant que la page n a pas abouti
|
|
324
|
+
var MARGE_PAGES=2; // on garde la page courante et deux de chaque cote
|
|
325
|
+
var DPR_MAX=2; // au-dela, le gain visuel est nul et le cout quadratique
|
|
326
|
+
var PIXELS_MAX=4e6; // plafond par canvas : ~4 M pixels = ~16 Mo de tampon
|
|
312
327
|
var onePage=false, soloOffered=false; // mode « une seule page » (présentation guidée) + offre de découverte solo
|
|
313
|
-
function setCur(p){ if(!p||p===cur)return; cur=p; T.setPage(p); try{ if(window.__soloEnd)window.__soloEnd(p); }catch(e){} var pg=document.getElementById('pg'); if(pg&&numPages)pg.textContent='Page '+p+' / '+numPages; if(PRES) pushPage(); }
|
|
328
|
+
function setCur(p){ if(!p||p===cur)return; cur=p; try{ evincerLoin(p); }catch(e){} T.setPage(p); try{ if(window.__soloEnd)window.__soloEnd(p); }catch(e){} var pg=document.getElementById('pg'); if(pg&&numPages)pg.textContent='Page '+p+' / '+numPages; if(PRES) pushPage(); }
|
|
314
329
|
// Bouton « Partager » : re-partage tracé (forward) → crée un lien ENFANT et propose de l'envoyer par email.
|
|
315
330
|
function wireShare(){
|
|
316
331
|
var pop=document.getElementById('pop'), btn=document.getElementById('shareBtn'), err=document.getElementById('shErr');
|
|
@@ -690,7 +705,12 @@ ${LEGAL_CSS}
|
|
|
690
705
|
}).catch(function(){ loadError("Impossible d'afficher ce document."); });
|
|
691
706
|
}
|
|
692
707
|
function build(){
|
|
693
|
-
|
|
708
|
+
// Une nouvelle construction invalide tout ce qui etait en vol : sans cela, les rendus lances
|
|
709
|
+
// par la precedente peignent dans des elements DETACHES (pagesEl a ete vide) et le travail est
|
|
710
|
+
// fait deux fois. C est le meme mecanisme de generation que la page audience.
|
|
711
|
+
renderGen++;
|
|
712
|
+
annulerTout();
|
|
713
|
+
rendered={}; enCours={};
|
|
694
714
|
if(io) io.disconnect();
|
|
695
715
|
if(ioCur) ioCur.disconnect();
|
|
696
716
|
// PRÉ-RENDU : marge de 500px → on rend les pages un peu avant qu'elles arrivent (fluide).
|
|
@@ -727,27 +747,78 @@ ${LEGAL_CSS}
|
|
|
727
747
|
var top=avg(2,3), bot=avg(Math.max(0,h-5),3);
|
|
728
748
|
scrollEl.style.background='linear-gradient(180deg,'+top+' 0%,'+top+' 42%,'+bot+' 58%,'+bot+' 100%)';
|
|
729
749
|
}catch(e){ /* canvas indisponible → fond par défaut */ } }
|
|
730
|
-
function
|
|
750
|
+
function annulerTout(){
|
|
751
|
+
for(var k in taches){ try{ taches[k].cancel(); }catch(e){} }
|
|
752
|
+
taches={};
|
|
753
|
+
}
|
|
754
|
+
// Rend une page a l etat de reserve : on retire le canvas et la couche texte, on remet la boite
|
|
755
|
+
// avec sa hauteur estimee. Le defilement ne bouge pas, et la page se re-rendra si on y revient.
|
|
756
|
+
function libererPage(n){
|
|
757
|
+
var el=pagesEl.querySelector('.page[data-p="'+n+'"]'); if(!el)return;
|
|
758
|
+
if(taches[n]){ try{ taches[n].cancel(); }catch(e){} delete taches[n]; }
|
|
759
|
+
var w=Math.round(targetWidth());
|
|
760
|
+
el.innerHTML=''; el.className='page ph'; el.textContent='Page '+n;
|
|
761
|
+
el.style.width=w+'px'; el.style.height=Math.round(w*firstAspect)+'px';
|
|
762
|
+
delete rendered[n]; delete enCours[n];
|
|
763
|
+
}
|
|
764
|
+
// ATTENTION : ON BORNE PAR UN BUDGET DE PIXELS, PAS SEULEMENT PAR UN NOMBRE DE PAGES.
|
|
765
|
+
// Deux pages A4 sur un ecran ×3 pesent plus que dix vignettes : compter les pages laisserait
|
|
766
|
+
// passer le cas qui fait tomber l onglet. On garde donc la fenetre courante, et on evince
|
|
767
|
+
// au-dela — le nombre de canvas reste borne quel que soit le parcours du document.
|
|
768
|
+
function evincerLoin(centre){
|
|
769
|
+
if(onePage) return; // une seule page affichee : rien a evincer
|
|
770
|
+
for(var k in rendered){
|
|
771
|
+
var n=+k;
|
|
772
|
+
if(n<centre-MARGE_PAGES || n>centre+MARGE_PAGES) libererPage(n);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
function renderPage(n,el){
|
|
776
|
+
if(rendered[n]||enCours[n])return;
|
|
777
|
+
// ATTENTION : ON NE MARQUE PAS « RENDUE » AVANT DE L AVOIR FAIT. rendered[n]=1 etait pose ici,
|
|
778
|
+
// donc un echec de getPage ou de render laissait la page marquee pour toujours : elle ne se
|
|
779
|
+
// retentait jamais et restait vide. On distingue « en cours » de « aboutie ».
|
|
780
|
+
enCours[n]=1;
|
|
781
|
+
var gen=renderGen;
|
|
731
782
|
if(IS_IMG){
|
|
732
783
|
var w=Math.round(targetWidth());
|
|
733
784
|
var im=document.createElement('img');
|
|
734
785
|
im.src=imgSrc; im.alt=''; im.style.width=w+'px'; im.style.display='block';
|
|
735
786
|
el.style.height=''; el.classList.remove('ph'); el.textContent=''; el.style.width=w+'px';
|
|
736
787
|
el.appendChild(im);
|
|
737
|
-
im.onload=function(){ hideLoader(); };
|
|
738
|
-
|
|
788
|
+
im.onload=function(){ if(gen!==renderGen)return; delete enCours[n]; rendered[n]=1; hideLoader(); };
|
|
789
|
+
im.onerror=function(){ delete enCours[n]; }; // reessayable
|
|
790
|
+
if(im.complete){ delete enCours[n]; rendered[n]=1; hideLoader(); }
|
|
739
791
|
return;
|
|
740
792
|
}
|
|
741
793
|
pdfDoc.getPage(n).then(function(page){
|
|
742
|
-
|
|
794
|
+
if(gen!==renderGen){ delete enCours[n]; return; } // un build() est passe : ce rendu n est plus attendu
|
|
795
|
+
// ATTENTION : LE DPR N ETAIT PAS PLAFONNE. Sur un ecran ×3, une page de 900 px de large fait
|
|
796
|
+
// pres de 10 M pixels, soit environ 39 Mo de tampon pour UNE page — le gain visuel au-dela de
|
|
797
|
+
// ×2 est nul, le cout est quadratique. On plafonne le DPR, puis on plafonne le NOMBRE DE
|
|
798
|
+
// PIXELS du canvas : deux bornes, parce que le zoom peut faire grandir la page independamment
|
|
799
|
+
// de la densite de l ecran.
|
|
800
|
+
var dpr=Math.min(DPR_MAX, window.devicePixelRatio||1);
|
|
743
801
|
var scale=Math.min(5,targetWidth()/page.getViewport({scale:1}).width);
|
|
744
802
|
var v=page.getViewport({scale:scale});
|
|
803
|
+
var pixels=Math.floor(v.width*dpr)*Math.floor(v.height*dpr);
|
|
804
|
+
if(pixels>PIXELS_MAX) dpr=Math.max(1, dpr*Math.sqrt(PIXELS_MAX/pixels));
|
|
745
805
|
var c=document.createElement('canvas');
|
|
746
806
|
c.setAttribute('role','img'); c.setAttribute('aria-label','Page '+n);
|
|
747
807
|
c.width=Math.floor(v.width*dpr); c.height=Math.floor(v.height*dpr); // backing store HD → net comme du natif
|
|
748
808
|
c.style.width=v.width+'px'; c.style.height=v.height+'px';
|
|
749
809
|
el.style.height=''; el.classList.remove('ph'); el.textContent=''; el.style.width=v.width+'px'; el.appendChild(c); // classList.remove (PAS className=) : ne pas écraser .cur — en mode une-page la page disparaissait une fois rendue (refit après réduction/réouverture du chat)
|
|
750
|
-
page.render({canvasContext:c.getContext('2d'),viewport:v,transform:dpr!==1?[dpr,0,0,dpr,0,0]:null})
|
|
810
|
+
var tache=page.render({canvasContext:c.getContext('2d'),viewport:v,transform:dpr!==1?[dpr,0,0,dpr,0,0]:null});
|
|
811
|
+
taches[n]=tache;
|
|
812
|
+
tache.promise.then(function(){
|
|
813
|
+
if(taches[n]===tache) delete taches[n];
|
|
814
|
+
if(gen!==renderGen){ delete enCours[n]; return; }
|
|
815
|
+
delete enCours[n]; rendered[n]=1; // AVEREE, pas seulement demandee
|
|
816
|
+
hideLoader(); if(n===cur)setTimeout(vsplitTint,60);
|
|
817
|
+
},function(){
|
|
818
|
+
// Annule ou en echec : on retire la marque pour que la page puisse etre retentee.
|
|
819
|
+
if(taches[n]===tache) delete taches[n];
|
|
820
|
+
delete enCours[n];
|
|
821
|
+
});
|
|
751
822
|
// Couche texte (sélection). En pdf.js v3, les spans utilisent font-size:calc(var(--scale-factor)*Npx)
|
|
752
823
|
// → SANS --scale-factor, taille nulle = pas de sélection. On le pose sur le conteneur.
|
|
753
824
|
try{ page.getTextContent().then(function(tc){
|
|
@@ -758,7 +829,7 @@ ${LEGAL_CSS}
|
|
|
758
829
|
catch(e){ try{ pdfjsLib.renderTextLayer({textContentSource:tc,container:tl,viewport:v}); }
|
|
759
830
|
catch(e2){ try{ pdfjsLib.renderTextLayer({textContent:tc,container:tl,viewport:v}); }catch(e3){} } }
|
|
760
831
|
}); }catch(e){}
|
|
761
|
-
}); }
|
|
832
|
+
},function(){ delete enCours[n]; }); }
|
|
762
833
|
// ── Assistant IA « présentateur » : chat requête/réponse (bot-start/bot-say) + saut de page piloté ──
|
|
763
834
|
${botOn && botBrowser ? botBrowser.botViewerJs(ICONS) : ""}
|
|
764
835
|
})();
|