From d6c7184475127529c0a213d73b0a96fb9e9a6ad5 Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 9 Sep 2026 16:40:31 +0000 Subject: [PATCH] =?UTF-8?q?Lecture=20:=20un=20=C3=A9chec=20de=20chargement?= =?UTF-8?q?=20de=20mpegts.js=20n'est=20plus=20fatal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit L'écran « Échec du chargement : vendor/mpegts.min.js » était un cul-de-sac : la moindre requête ratée sur la lib condamnait la chaîne, alors que le flux restait parfaitement lisible autrement. - xf-player-core : le chargement des libs est retenté une fois en contournant le cache HTTP (les libs sont servies en max-age=86400, une entrée tronquée bloquait le lecteur jusqu'au vidage manuel du cache), et seul un succès est mémorisé — la promesse rejetée restait en cache et rendait tout réessai impossible pour le reste de la session. - Repli automatique : si mpegts.js reste introuvable, le live bascule sur la route HLS équivalente (où FFmpeg réencode déjà l'audio en AAC) ; si hls.js reste introuvable, on tente le HLS natif. Plus d'écran d'erreur quand un chemin de lecture est encore disponible. - Message d'erreur exploitable : la cause réelle est affichée (HTTP 404, 429, réseau injoignable) au lieu d'un échec indifférencié. - Préchargement corrigé : les trois players préchargeaient hls.js en dur, soit 618 Ko téléchargés pour rien à chaque zap TV — c'est mpegts.js qui sert au direct — au détriment du flux et du chargement de mpegts.js lui-même. Le préchargement suit désormais le flux demandé, avec la même décision que XFPlayer.start() (et ne charge rien sur Safari/iOS). - Serveur : avertissement explicite au démarrage si web/vendor/*.min.js manque, au lieu de laisser le navigateur échouer sans explication. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014FSovdudi3KBwZK3aC42fw --- CHANGELOG.md | 3 ++ bin/server.dart | 14 +++++++ web/player.html | 30 ++++++++++++++- web/player_lite.html | 30 ++++++++++++++- web/player_mobile.html | 30 ++++++++++++++- web/xf-player-core.js | 85 +++++++++++++++++++++++++++++++++++++++--- 6 files changed, 183 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cfb3be..8099e60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ - **Players mis à jour** : hls.js 1.6.7 → 1.7.1, mpegts.js 1.7.3 → 1.8.2 (vendorisés) - **Démarrage plus rapide** : sonde FFmpeg bornée (`-fflags nobuffer`, probesize réduit) sur le live et le turbo ; probesize VOD 10 Mo → 5 Mo ; preset live `high` et lecture d'enregistrement en `veryfast` (medium ne tenait pas le temps réel) ; détection de playlist toutes les 100 ms au lieu de 500 ms ; suppression du cache-buster qui re-téléchargeait player.html à chaque zap (les .html passent en no-cache serveur) - **Latence live maîtrisée** : rattrapage du direct activé dans mpegts.js (profil rapide) — les micro-coupures ne font plus dériver la lecture derrière le direct +- **Fix « Échec du chargement : vendor/mpegts.min.js »** : un échec de chargement d'une lib de lecture n'affiche plus un écran d'erreur définitif. Le chargement est retenté une fois en contournant le cache HTTP (une entrée tronquée condamnait le lecteur jusqu'au vidage manuel du cache), le résultat n'est mémorisé qu'en cas de succès (une promesse rejetée en cache rendait tout réessai impossible) et, si la lib reste introuvable, le live bascule automatiquement sur la route HLS équivalente. Le message affiché précise désormais la cause (HTTP 404, 429, réseau injoignable) +- **Préchargement de la bonne lib** : les trois players préchargeaient hls.js en dur, soit 618 Ko téléchargés pour rien à chaque zap TV — où c'est mpegts.js qui sert — au détriment du flux et du chargement de mpegts.js. Le préchargement suit maintenant le flux réellement demandé (et ne charge rien sur Safari/iOS, qui lit le HLS nativement) +- **Alerte au démarrage** : le serveur signale explicitement l'absence de `web/vendor/*.min.js` au lancement, au lieu de laisser le navigateur échouer sans explication ### 🧰 Qualité / Infra - **Builds reproductibles** : `pubspec.lock` (frontend et backend) désormais versionnés et utilisés par le Dockerfile — chaque build résolvait jusqu'ici des versions fraîches diff --git a/bin/server.dart b/bin/server.dart index 574bf19..e0f3209 100644 --- a/bin/server.dart +++ b/bin/server.dart @@ -259,6 +259,20 @@ void main(List args) async { }), ); + // Les libs de lecture sont servies depuis l'image, plus depuis un CDN : si + // elles manquent (build web incomplet, copie Docker partielle), le lecteur + // échoue côté navigateur avec un simple « Échec du chargement » sans que + // rien ne l'ait signalé au démarrage. On le dit ici, une fois, clairement. + for (final lib in const ['hls.min.js', 'mpegts.min.js']) { + final file = File('$webPath/vendor/$lib'); + if (!file.existsSync()) { + print( + '[Server] ATTENTION: web/vendor/$lib absent de $webPath — ' + 'la lecture échouera dans le navigateur.', + ); + } + } + // Create static handler final baseStaticHandler = createStaticHandler( webPath, diff --git a/web/player.html b/web/player.html index 2da6540..f84ed7d 100644 --- a/web/player.html +++ b/web/player.html @@ -11,7 +11,35 @@ et environ 200 Ko de JS jamais téléchargés dans le cas courant. --> - + diff --git a/web/player_lite.html b/web/player_lite.html index e32e07b..406e42b 100644 --- a/web/player_lite.html +++ b/web/player_lite.html @@ -5,7 +5,35 @@ XtremFlow Player Lite - + diff --git a/web/player_mobile.html b/web/player_mobile.html index b780168..89b7827 100644 --- a/web/player_mobile.html +++ b/web/player_mobile.html @@ -8,7 +8,35 @@ XtremFlow Player Mobile - + diff --git a/web/xf-player-core.js b/web/xf-player-core.js index 95902ca..d4d0a0f 100644 --- a/web/xf-player-core.js +++ b/web/xf-player-core.js @@ -60,17 +60,67 @@ // ------------------------------------------------------------------ var _scriptCache = {}; - function loadScript(src) { - if (_scriptCache[src]) return _scriptCache[src]; - _scriptCache[src] = new Promise(function (resolve, reject) { + function _delay(ms) { + return new Promise(function (resolve) { setTimeout(resolve, ms); }); + } + + function _injectScript(src) { + return new Promise(function (resolve, reject) { var s = document.createElement('script'); s.src = src; s.async = true; - s.onload = resolve; - s.onerror = function () { reject(new Error('Échec du chargement : ' + src)); }; + s.onload = function () { resolve(); }; + s.onerror = function () { + // Laisser une balise morte dans le ferait échouer une + // éventuelle nouvelle tentative sur le même src. + if (s.parentNode) s.parentNode.removeChild(s); + reject(new Error('script error')); + }; document.head.appendChild(s); }); - return _scriptCache[src]; + } + + /** + * Précise la cause d'un échec pour le message affiché et les logs : + * 404 = lib absente de l'image déployée, 429 = quota du rate limiter, + * échec réseau = lien coupé. Sans cela, toutes les causes se + * ressemblaient à l'écran. + */ + function _describeFailure(src) { + if (typeof fetch !== 'function') return Promise.resolve(''); + return fetch(src, { credentials: 'same-origin', cache: 'no-store' }) + .then(function (r) { return r.ok ? '' : ' (HTTP ' + r.status + ')'; }) + .catch(function () { return ' (réseau injoignable)'; }); + } + + /** + * Charge une lib vendorisée, avec une seconde tentative qui contourne le + * cache HTTP : les libs sont servies en `max-age=86400`, donc une entrée + * tronquée par une coupure réseau condamnait le lecteur jusqu'au vidage + * manuel du cache navigateur. Seuls les succès sont mémorisés — une + * promesse rejetée gardée en cache rendait tout réessai impossible pour + * le reste de la session. + */ + function loadScript(src) { + if (_scriptCache[src]) return _scriptCache[src]; + + var attempt = _injectScript(src) + .catch(function () { + return _delay(400).then(function () { + return _injectScript( + src + (src.indexOf('?') === -1 ? '?' : '&') + 'reload=' + Date.now() + ); + }); + }) + .catch(function () { + return _describeFailure(src).then(function (why) { + throw new Error('Échec du chargement : ' + src + why); + }); + }); + + _scriptCache[src] = attempt; + attempt.catch(function () { delete _scriptCache[src]; }); + return attempt; } /** Ouvre la connexion vers l'hôte du flux avant même de connaître l'URL finale. */ @@ -253,6 +303,11 @@ } }); }).catch(function (err) { + // hls.js indisponible : plutôt que d'échouer, tenter les chemins qui + // ne demandent aucune lib (HLS natif Safari/iOS, sinon lecture directe + // si le navigateur sait décoder le conteneur). + self.log(err.message); + if (self._canPlayNativeHls()) { self._playDirect(self.url); return; } self.onError(err.message); }); }; @@ -267,6 +322,24 @@ } self._createMpegts(isLive); }).catch(function (err) { + // mpegts.js indisponible (build incomplet, coupure réseau, quota du + // rate limiter…) : le même flux reste lisible par la route HLS, où + // FFmpeg réencode déjà l'audio en AAC. Une seconde de latence en plus + // vaut mieux qu'un écran d'erreur sur une chaîne parfaitement lisible. + var hlsUrl = self._hlsEquivalent(self.url); + self.log(err.message + (hlsUrl ? ' — bascule sur le chemin HLS' : '')); + if (hlsUrl) { + self.url = hlsUrl; + self.onLoading('Ouverture du flux'); + if (self._canPlayNativeHls()) { self._playDirect(hlsUrl); return; } + return self._startHls(); + } + // Aucun équivalent HLS (flux direct fourni par la playlist) : dernier + // recours, laisser le navigateur décoder lui-même le conteneur. + if (self.video.canPlayType('video/mp2t') !== '') { + self._playDirect(self.url); + return; + } self.onError(err.message); }); };