discovery-media-player 0.1.29 → 0.1.31

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/server/handler.js +87 -11
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "discovery-media-player",
3
- "version": "0.1.29",
3
+ "version": "0.1.31",
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",
package/server/handler.js CHANGED
@@ -241,11 +241,14 @@ var Live=(function(){
241
241
  function toggleMute(){ MUTED=!MUTED; try{ localStorage.setItem('3dd-present-mute',MUTED?'1':'0'); }catch(e){} if(MUTED)hidePeek(); applyMute(); }
242
242
  // Flèche « envoyer » : visible seulement si le champ contient du texte et qu'on peut poster.
243
243
  function toggleSend(){ var s=document.getElementById('chatSend'),t=document.getElementById('chatText'); if(!s||!t)return; var can=!(LOCKED&&!canMod()); s.classList.toggle('on', can && (t.value||'').trim().length>0); }
244
- function sendMap(p){try{if(ch)ch.send({type:'broadcast',event:'map',payload:p});}catch(e){}}
244
+ // Un SIGNAL, plus une position : la charge utile était crue par l'audience sur un canal public.
245
+ function sendMap(){try{if(ch)ch.send({type:'broadcast',event:'map',payload:{}});}catch(e){}}
245
246
  function onMap(fn){_onMap=fn;}
246
247
  // État de la présentation diffusé par le présentateur — même canal que la carte. Sert à se
247
248
  // passer de la lecture anonyme des tables : l'audience n'a plus besoin de lire la ligne.
248
- function sendState(p){try{if(ch)ch.send({type:'broadcast',event:'state',payload:p});}catch(e){}}
249
+ // Un SIGNAL, pas un état. L'audience relit depuis 0.1.19 et ignore déjà cette charge ; la laisser
250
+ // partir donnait l'illusion qu'elle sert, et invitait le prochain à s'en resservir.
251
+ function sendState(){try{if(ch)ch.send({type:'broadcast',event:'state',payload:{}});}catch(e){}}
249
252
  // CHAT EN DIFFUSION. Les messages arrivaient jusqu'ici par la lecture de TABLE en temps réel,
250
253
  // qui exige que cette table soit lisible publiquement — donc, avec la clé publiable, les
251
254
  // conversations de TOUTES les présentations, pas seulement la sienne. C'était le dernier
@@ -476,6 +479,10 @@ var Live=(function(){
476
479
  },{minMs:400});
477
480
  }
478
481
  var _ordEtat=relireAvec('&state=1',function(d){if(d.state)etatDuServeur(d.state);});
482
+ // ⚠️ Exposé hors de cette fermeture : la carte vit dans un AUTRE bloc de script et doit
483
+ // pouvoir déclencher la relecture. Même raison que window.__presAppliquerEtat — un nom
484
+ // référencé depuis la mauvaise portée part dans un catch muet, et l'audience se fige.
485
+ window.__presRelireEtat=function(){_ordEtat.signaler();};
479
486
  function relireEtat(){_ordEtat.signaler();}
480
487
  function relireChat(){_ordChat.signaler();}
481
488
  var _ordChat=relireAvec('&chat=1',function(d){
@@ -517,7 +524,10 @@ var Live=(function(){
517
524
  ch.on('broadcast',{event:'lock'},function(){relireChat();});
518
525
  ch.on('broadcast',{event:'state'},function(){relireEtat();});
519
526
  ch.on('broadcast',{event:'typing'},function(p){onTyping(p&&p.payload);});
520
- ch.on('broadcast',{event:'map'},function(p){if(_onMap&&p&&p.payload)_onMap(p.payload);});
527
+ // ⚠️ AUCUNE CHARGE NE PASSE. Elle est ignorée plus loin, mais s'arrêter là serait une défense
528
+ // par accident : le jour où quelqu'un rebranche un paramètre, la charge d'un canal public
529
+ // redeviendrait crue sans que rien ne le signale. On coupe le chemin, pas seulement l'usage.
530
+ ch.on('broadcast',{event:'map'},function(){if(_onMap)_onMap();});
521
531
  ch.subscribe(function(st){if(st==='SUBSCRIBED'){ch.track({name:me.name,email:me.email,avatar:me.avatar,role:me.role,member:!!me.member,uid:attKey(me)});sendAttend();}});
522
532
  _tyIv=setInterval(renderTyping,1500);
523
533
  _atIv=setInterval(sendAttend,25000);
@@ -592,8 +602,34 @@ var Map3DD=(function(){
592
602
  function state(){ if(!map)return null; if(useG){ var c=map.getCenter(); return {kind:'map',center:[c.lat(),c.lng()],zoom:map.getZoom(),marker:marker?[marker.getPosition().lat(),marker.getPosition().lng()]:null,mapType:map.getMapTypeId()}; }
593
603
  var c2=map.getCenter(); return {kind:'map',center:[c2.lat,c2.lng],zoom:map.getZoom(),marker:marker?[marker.getLatLng().lat,marker.getLatLng().lng]:null}; }
594
604
  function setCenterZoom(ll,z){ if(useG){ map.setCenter({lat:ll[0],lng:ll[1]}); map.setZoom(z); } else { map.setView(ll,z); } }
595
- function broadcast(){ var n=Date.now(); if(n-_bcT<100||!map)return; _bcT=n; var s=state(); if(s){try{ if(window.Live) Live.sendMap(s); }catch(e){}} }
596
- function schedPersist(){ clearTimeout(_psT); _psT=setTimeout(function(){ if(map&&persist)persist(state()); },700); }
605
+ // ⚠️ LA POSITION NE VOYAGE PLUS DANS LA DIFFUSION.
606
+ //
607
+ // Elle y voyageait, et l'audience l'appliquait telle quelle. Le canal étant public, n'importe quel
608
+ // participant déplaçait donc la carte de tout le monde, avec les coordonnées de son choix. C'était
609
+ // assumé en 0.1.19 au motif que le signal est « éphémère et sans vérité serveur » — l'argument ne
610
+ // tient pas : PENDANT UN MODE CARTE, CE SIGNAL EST L'IMAGE QUE VOIT L'AUDIENCE. 'typing' peut
611
+ // rester cosmétique, 'map' non. (audit P0-1)
612
+ //
613
+ // Le présentateur persiste sa position (route gatée par JWT), puis émet un signal VIDE. L'audience
614
+ // relit l'état et applique ce que le serveur lui donne. Un participant hostile peut toujours
615
+ // émettre le signal : il provoque une relecture, et n'obtient rien.
616
+ //
617
+ // ⚠️ ORDONNANCEUR ET NON DEBOUNCE, et c'est le cœur du problème. schedPersist repoussait
618
+ // l'écriture de 700 ms à chaque mouvement : pendant un déplacement CONTINU, elle ne partait
619
+ // jamais. C'est précisément pour ça que la position voyageait dans la diffusion. Un ordonnanceur
620
+ // écrit au plus une fois par 500 ms ET sert toujours la dernière position — donc l'audience suit
621
+ // pendant le mouvement, pas seulement à l'arrêt.
622
+ //
623
+ // Le suivi devient PAR PALIERS au lieu d'être continu (environ deux fois par seconde). C'est le
624
+ // prix pour que personne d'autre que le présentateur ne pilote l'écran de l'audience.
625
+ var _ordPersist=null;
626
+ function persistOrd(){ if(!_ordPersist&&window.Player&&Player.live&&Player.live.createScheduler){
627
+ _ordPersist=Player.live.createScheduler(function(fini){ try{ if(map&&persist)persist(state()); }catch(e){} fini(); },{minMs:500}); }
628
+ return _ordPersist; }
629
+ function broadcast(){ if(!map)return; var n=Date.now(); if(n-_bcT<200)return; _bcT=n;
630
+ var o=persistOrd(); if(o)o.signaler();
631
+ try{ if(window.Live) Live.sendMap(); }catch(e){} }
632
+ function schedPersist(){ var o=persistOrd(); if(o)o.signaler(); else { clearTimeout(_psT); _psT=setTimeout(function(){ if(map&&persist)persist(state()); },700); } }
597
633
  function enter(content,presenter,persistFn){ isPres=!!presenter; if(persistFn)persist=persistFn;
598
634
  if(content&&content.mapType)mapType=content.mapType;
599
635
  var wrap=document.getElementById('mapWrap'); var on=wrap&&wrap.classList.contains('on');
@@ -632,7 +668,14 @@ var Map3DD=(function(){
632
668
  function goSV(center){ if(!GMAPS_KEY){tempHint('Street View indisponible.');return;} tempHint('Recherche Street View…');
633
669
  loadGoogle(function(){ try{ new google.maps.StreetViewService().getPanorama({location:{lat:center[0],lng:center[1]},radius:80},function(data,status){ if(status==='OK'&&data&&data.location){ var ll=data.location.latLng; var content={kind:'streetview',position:[ll.lat(),ll.lng()],pov:{heading:0,pitch:0},zoom:1}; if(persist)persist(content); enterSV(content,true,persist); } else { tempHint('Pas de Street View à cet endroit.'); } }); }catch(e){ tempHint('Street View indisponible.'); } }); }
634
670
  function svState(){ if(!pano)return null; var p=pano.getPosition(),v=pano.getPov(); if(!p)return null; return {kind:'streetview',position:[p.lat(),p.lng()],pov:{heading:v.heading,pitch:v.pitch},zoom:pano.getZoom()}; }
635
- function svBcast(){ var n=Date.now(); if(n-_svBcT<120)return; _svBcT=n; var s=svState(); if(!s)return; try{ if(window.Live)Live.sendMap(s); }catch(e){} clearTimeout(_psT); _psT=setTimeout(function(){ if(persist&&pano)persist(svState()); },800); }
671
+ // Même règle qu'au-dessus : on persiste, on signale, on ne transporte pas la position.
672
+ var _ordSv=null;
673
+ function svOrd(){ if(!_ordSv&&window.Player&&Player.live&&Player.live.createScheduler){
674
+ _ordSv=Player.live.createScheduler(function(fini){ try{ if(persist&&pano)persist(svState()); }catch(e){} fini(); },{minMs:500}); }
675
+ return _ordSv; }
676
+ function svBcast(){ var n=Date.now(); if(n-_svBcT<200)return; _svBcT=n; if(!svState())return;
677
+ var o=svOrd(); if(o)o.signaler();
678
+ try{ if(window.Live)Live.sendMap(); }catch(e){} }
636
679
  function svApply(p){ if(!pano||!p)return; try{ if(p.position)pano.setPosition({lat:p.position[0],lng:p.position[1]}); if(p.pov)pano.setPov({heading:p.pov.heading||0,pitch:p.pov.pitch||0}); if(typeof p.zoom!=='undefined')pano.setZoom(p.zoom); }catch(e){} }
637
680
  function ensurePano(content){ var el=document.getElementById('svPano'); if(!el||!window.google||!window.google.maps)return;
638
681
  var pos=content&&content.position?{lat:content.position[0],lng:content.position[1]}:{lat:48.8584,lng:2.2945};
@@ -1934,9 +1977,36 @@ ${LEGAL_CSS}
1934
1977
  function showBar(slug){ var lk=document.getElementById('pbarLink'); if(lk) lk.value=location.origin+'/present/'+slug; var pb=document.getElementById('pbar'); if(pb) pb.style.display='flex'; }
1935
1978
  // Le présentateur DIFFUSE l'état qu'il vient de persister. La base reste la vérité (les
1936
1979
  // arrivants tardifs la relisent) ; la diffusion évite à l'audience de lire la table.
1937
- function diffuserEtat(extra){ if(!PRES||!window.Live)return; try{ Live.sendState(Object.assign({active:true,current_page:cur||1,file_url:CFG.present&&CFG.present.url||null,updated_at:new Date().toISOString()},extra||{})); }catch(e){} }
1938
- function pushPage(){ if(!PRES)return; clearTimeout(_pushT); _pushT=setTimeout(function(){ if(!PRES)return; diffuserEtat({current_page:cur||1}); fetch('/api/doc',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'present-page',slug:PRES.slug,control:PRES.control,page:cur||1})}).catch(function(){}); },250); }
1939
- function endPresent(){ if(!PRES)return; diffuserEtat({active:false}); var p=PRES; PRES=null; clearInterval(_hbIv); clearCtl(p.slug); var pb=document.getElementById('pbar'); if(pb)pb.style.display='none'; try{ if(window.Live) Live.disconnect(); }catch(e){} try{ var b=JSON.stringify({action:'present-end',slug:p.slug,control:p.control}); if(navigator.sendBeacon){navigator.sendBeacon('/api/doc',new Blob([b],{type:'application/json'}));} else {fetch('/api/doc',{method:'POST',headers:{'Content-Type':'application/json'},body:b,keepalive:true});} }catch(e){} }
1980
+ // ⚠️ ÉCRIRE, PUIS SIGNALER — ET PAS L'INVERSE.
1981
+ //
1982
+ // Le signal partait AVANT l'écriture. L'audience relisait donc l'état pendant que la base
1983
+ // portait encore l'ancien, et aucun second signal n'était garanti : la page tournée se perdait
1984
+ // jusqu'au filet de resynchronisation. Le commentaire d'origine affirmait pourtant l'ordre
1985
+ // inverse — il décrivait l'intention, pas le code.
1986
+ //
1987
+ // Depuis 0.1.19 le signal ne sert qu'à dire « relis » : le retarder d'un aller-retour ne coûte
1988
+ // rien, alors que l'émettre trop tôt fait relire pour rien ET perdre le changement.
1989
+ // (audit P0-3)
1990
+ function diffuserEtat(){ if(!PRES||!window.Live)return; try{ Live.sendState(); }catch(e){} }
1991
+ function pushPage(){ if(!PRES)return; clearTimeout(_pushT); _pushT=setTimeout(function(){ if(!PRES)return;
1992
+ fetch('/api/doc',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'present-page',slug:PRES.slug,control:PRES.control,page:cur||1})})
1993
+ .then(function(r){ if(r&&r.ok)diffuserEtat(); })
1994
+ .catch(function(){});
1995
+ },250); }
1996
+ // ⚠️ TROIS GESTES, ET L'ORDRE ÉTAIT LE PIRE DES SIX. On signalait la fin, puis on COUPAIT LE
1997
+ // CANAL, puis on envoyait l'avis de fin. Le signal partait donc avant l'écriture (relecture sur
1998
+ // un état périmé), et la coupure avant l'envoi. L'audience pouvait ne jamais apprendre que la
1999
+ // présentation était terminée — jusqu'au filet, 25 s plus tard, ou jamais si elle s'était
2000
+ // rechargée entre-temps.
2001
+ //
2002
+ // sendBeacon ne s'attend pas, mais il rend la main une fois la requête MISE EN FILE : signaler
2003
+ // juste après, puis couper, respecte l'ordre autant que ce transport le permet.
2004
+ function endPresent(){ if(!PRES)return; var p=PRES; PRES=null; clearInterval(_hbIv); clearCtl(p.slug);
2005
+ var pb=document.getElementById('pbar'); if(pb)pb.style.display='none';
2006
+ try{ var b=JSON.stringify({action:'present-end',slug:p.slug,control:p.control});
2007
+ if(navigator.sendBeacon){navigator.sendBeacon('/api/doc',new Blob([b],{type:'application/json'}));}
2008
+ else {fetch('/api/doc',{method:'POST',headers:{'Content-Type':'application/json'},body:b,keepalive:true});} }catch(e){}
2009
+ try{ if(window.Live){ Live.sendState(); Live.disconnect(); } }catch(e){} }
1940
2010
  // Transfert : je passe la main → je cesse de piloter SANS clôturer la présentation (le nouvel owner reprendra).
1941
2011
  function stopPilotingLocally(){ if(!PRES)return; var s=PRES.slug; PRES=null; clearInterval(_hbIv); clearCtl(s); var pb=document.getElementById('pbar'); if(pb)pb.style.display='none'; try{ if(window.Live) Live.disconnect(); }catch(e){} }
1942
2012
  function liveConnect(slug,control){ try{ if(window.Live){ var P=CFG.present||{}; Live.connect(slug,{name:P.by||'Présentateur',email:P.email||'',avatar:P.av||'',role:'presenter',member:true},control); } }catch(e){} }
@@ -1967,7 +2037,10 @@ ${LEGAL_CSS}
1967
2037
  }).catch(function(){});
1968
2038
  }
1969
2039
  // Carte live : persiste le contenu (present-content, JWT) → l'audience bascule/suit via Realtime.
1970
- function presentContent(content){ if(!PRES)return; diffuserEtat({content:content||null}); var h={'Content-Type':'application/json'}; var tk=appToken(); if(tk) h['Authorization']='Bearer '+tk; fetch('/api/doc',{method:'POST',headers:h,body:JSON.stringify({action:'present-content',slug:PRES.slug,content:content})}).catch(function(){}); }
2040
+ function presentContent(content){ if(!PRES)return; var h={'Content-Type':'application/json'}; var tk=appToken(); if(tk) h['Authorization']='Bearer '+tk;
2041
+ fetch('/api/doc',{method:'POST',headers:h,body:JSON.stringify({action:'present-content',slug:PRES.slug,content:content})})
2042
+ .then(function(r){ if(r&&r.ok)diffuserEtat(); })
2043
+ .catch(function(){}); }
1971
2044
  function showMap(){ if(!PRES||!window.Map3DD)return; var wrap=document.getElementById('mapWrap'); if(wrap&&wrap.classList.contains('on')){ Map3DD.enter(null,true,presentContent); return; } var init=Player.presentation.initialMapContent(); presentContent(init); Map3DD.enter(init,true,presentContent); }
1972
2045
  function hideMap(){ if(!window.Map3DD)return; Map3DD.exit(); presentContent(null); }
1973
2046
  // Mode INTÉGRÉ : la page hôte délègue sa barre de titre à celle-ci. On lui dit
@@ -2374,7 +2447,10 @@ function presentHtml(pres, nonce, logoUrl, supaUrl, supaKey) {
2374
2447
  (function(){
2375
2448
  var slug=${JSON.stringify(pres.slug)};
2376
2449
  // Carte live : suivre en direct les mouvements du présentateur (broadcast).
2377
- try{ if(window.Live&&window.Map3DD) Live.onMap(function(p){ Map3DD.apply(p); }); }catch(e){}
2450
+ // ⚠️ ON NE CROIT PLUS LA CHARGE. Le signal dit « quelque chose a bougé » ; ce qui a bougé vient
2451
+ // de la relecture d'état, gatée par l'écriture du présentateur. Un participant hostile peut
2452
+ // émettre : il déclenche une relecture bornée, et n'obtient rien.
2453
+ try{ if(window.Live&&window.Map3DD) Live.onMap(function(){ if(window.__presRelireEtat)window.__presRelireEtat(); }); }catch(e){}
2378
2454
  // L'état arrive maintenant par DEUX voies : la table (historique) et la diffusion du
2379
2455
  // présentateur (nouvelle). Les deux passent par le même filtre, qui ignore un état déjà
2380
2456
  // appliqué — recevoir deux fois la même chose ne doit pas re-rendre la page.