Télémétrie (session de 25 min avec la réserve de 25 s) : un silence du
panneau a été absorbé sans coupure (avance 27 → 14,7 s), mais à la
reprise le panneau renvoie d'un coup tout le retard. L'avance dépassait
alors le seuil de rattrapage (45 s) et le lecteur sautait 20 s de
programme — deux fois en une minute.
Saut seulement au-delà de 90 s de retard ; liveSync (x1,1 au-delà de
35 s) résorbe l'excédent en douceur. Vidéo déjà vue purgée plus tôt
(30 s) pour rester sous le quota du SourceBuffer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Télémétrie d'un freeze en prod : le panneau s'est tu ~15-20 s sur une
connexion restée ouverte (aucune reconnexion FFmpeg dans les logs).
Le lecteur n'avait gardé que 11-16 s d'avance — il « consommait » à x1,1
celle que le panneau envoie au démarrage pour revenir à 12 s — puis,
après la coupure de 6,3 s, il repartait avec 1 s de stock et recoupait
aussitôt (1,7 s puis 2,0 s).
- réserve visée 25 s (liveSync 25/35, saut au-delà de 45 s) : ~25 s de
retard sur le direct en échange de la traversée de ces silences
- après une coupure, attente de 4 s de réserve avant de relire (comme
mpv cache-pause), 15 s au plus ; une pause utilisateur l'annule ; le
lecteur lite affiche « Reconstitution de la réserve » au lieu du
bouton lecture
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Les freezes (image et son figés puis reprise) se produisent dans le
navigateur de l'utilisateur ; le serveur n'en voyait rien. Le lecteur
envoie désormais chaque coupure (durée, buffer), saut (de → vers),
erreur MPEG-TS/HLS, recréation, repli HLS et changement de profil, plus
un bilan toutes les 60 s, à POST /api/player-log (authentifié). Le
serveur écrit une ligne « [Player] … » par événement.
Entrée filtrée : événements et champs connus seulement, texte borné,
URL masquées (une erreur réseau peut recopier les identifiants Xtream),
pas d'injection de lignes. Envoi jamais bloquant pour la lecture.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mesure après déploiement : premier octet de turbo.ts à 4,5–4,8 s avec la
source HLS, contre 4,3–4,7 s en .ts — aucun gain. Les logs confirment
que le HLS était bien utilisé (aucun repli). La sonde qui annonçait
0,5 s ouvrait le .ts AVANT le .m3u8 : la chaîne était déjà démarrée chez
le panneau. Les ~4,5 s sont le temps de démarrage d'une chaîne chez le
fournisseur, quel que soit le format.
Retour au .ts, déjà validé en lecture, plutôt que de garder un chemin
plus complexe (segments de 10 s, repli) sans bénéfice mesuré.
This reverts commit 0ea60cc.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mesuré en prod après le premier correctif : 69 chaînes sur 149 avec
guide. Les vides restantes relevaient surtout de formes de nom que le
dump écrit autrement, ou de pays non couverts :
- « F3 ALPES » → France 3 Alpes (France.3.-.Alpes.fr) : F2 à F5 développés
- clé souple en dernier recours, dans son propre espace de noms :
sans code pays ni « channel/tv/hd » (« AB3 » → AB3.Channel.fr)
- chaîne « +1 » absente du dump (TF1 +1) : guide de la chaîne principale
décalé d'une heure ; une +1 qui a son propre guide le garde
- sources par défaut : FR1 puis CH1 (RTS…) et BE2 (même fournisseur),
le dump français reste prioritaire
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mesuré en prod avec la sonde admin : le panneau met 4,2 à 4,4 s à
répondre la redirection d'un .ts, contre 0,2 à 1,3 s pour un .m3u8 dont
le premier segment arrive ensuite en 50 ms.
turbo.ts et les sessions HLS live lisent désormais le .m3u8 du panneau
(-live_start_index -2 : deux segments d'avance d'un coup), avec repli
automatique sur le .ts si le HLS ne livre rien — dans la même réponse
pour turbo.ts, le lecteur ne voit qu'un démarrage plus long. Pas de
reconnect_at_eof en HLS : chaque segment finit par un EOF (leçon de
xtremobile). Le navigateur reçoit toujours le même flux MPEG-TS.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
~4,3 s des ~4,6 s d'un zap se passent avant le premier octet du panneau.
Le compte autorise aussi le HLS (allowed_output_formats : m3u8, ts) ;
GET /api/admin/upstream-probe?stream=<id> mesure, pour .ts et .m3u8,
chaque redirection, les en-têtes, le premier octet et le premier segment,
pour décider du format à utiliser. Admin uniquement ; seuls l'hôte et les
durées sont rendus, jamais les identifiants ni les jetons.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sans charset annoncé, Response.body pouvait décoder le JSON du panneau en
latin-1 et abîmer les noms accentués ou décorés (« Chérie », « ◉ »), donc
leur correspondance avec le guide XMLTV. Le test simule désormais des
octets UTF-8 sans charset, comme le panneau réel.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Constaté en prod : chaque /api/epg répondait 500 en 4 s. Un appel
player_api qui lève (connexion coupée, délai) n'était pas rattrapé, et le
dump XMLTV externe, qui couvre pourtant TF1, France 2, M6…, n'était jamais
consulté. Et 59 % des chaînes françaises n'ont pas d'epg_channel_id : leur
nom brut (« FR - TF1 FHD ◉ ») ne correspondait à aucune entrée XMLTV.
- échec player_api rattrapé : la chaîne passe à la source suivante
- dump externe (déjà en mémoire) avant l'appel lent chaîne par chaîne
- correspondance par nom nettoyé : préfixe pays, FHD/HD/UHD/4K/HEVC…,
symboles retirés, forme « nom.pays » tentée ; « +1 » conservé
- tous les display-name XMLTV indexés, plus seulement le premier
- table des chaînes : un seul téléchargement partagé, décodé hors de la
boucle principale, et un échec n'est plus gardé 6 h (2 min)
- guide vide gardé 5 min au lieu de 30
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AccountLimits interrogeait player_api.php avant d'ouvrir le flux quand
sa valeur n'était pas en cache (premier flux, puis toutes les 10 min).
Le panneau met jusqu'à 4 s à répondre (mesuré en prod) : autant de
retard au démarrage d'une chaîne ou d'un film.
La valeur connue, même périmée, ou le repli (1) est rendue tout de suite ;
la lecture chez le panneau se fait en arrière-plan, une seule à la fois.
Sans risque : le quota ne sert qu'à choisir quels flux orphelins couper.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
La règle « le dernier flux gagne » coupait aussi un flux en cours de
lecture. Deux lecteurs ouverts sur un compte à une connexion se coupaient
alors en boucle (chacun relance aussitôt) : plus aucune image nulle part.
Constaté en prod : tant qu'un autre onglet lisait, chaque ouverture
échouait après 5 s sans un octet.
Une connexion n'est désormais coupée que si son spectateur ne donne plus
signe de vie depuis 15 s : aucune requête de playlist/segment (HLS, film)
ni octet remis au client (turbo.ts). Les orphelins partent toujours, un
second lecteur actif est refusé par le panneau comme avant.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Le serveur réclamait toujours <id>.mkv au panneau. Un film stocké en .mp4
était refusé (HTTP 551 constaté en prod). Le client transmet désormais
container_extension (?ext=), validé côté serveur ; mkv reste le défaut
pour un client qui ne l'envoie pas. Même approche que xtremobile, qui lit
ces films sans souci.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main.dart.js, flutter_bootstrap.js et xf-player-core.js gardent le même
nom d'une build à l'autre. En prod, Nginx Proxy Manager les met en cache
(Cache Assets) jusqu'à 12 h, même sur rechargement forcé : après le
dernier déploiement, l'ancienne app et l'ancien lecteur tournaient avec
le nouveau serveur.
Au démarrage, le serveur réécrit index.html, flutter_bootstrap.js et
player*.html pour référencer ces fichiers avec ?v=<empreinte du contenu> :
chaque version a sa propre URL, aucun cache ne peut les confondre.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
L'abonnement n'autorise qu'une connexion simultanée (max_connections = 1,
vérifié en prod). Or un FFmpeg de lecture survivait au spectateur : 4 min
pour un direct HLS, jusqu'à 15 min ou la fin du téléchargement pour un
film, le temps de détecter la déconnexion pour turbo.ts. Le flux suivant
trouvait le slot pris : HTTP 551 ou timeout côté panneau (film qui ne
démarre pas, zap qui échoue).
Avant d'ouvrir une connexion amont, le serveur coupe désormais les plus
anciennes connexions de lecture du même compte, dans la limite du quota
max_connections lu chez le fournisseur (cache 10 min, 1 par défaut). Les
enregistrements ne sont jamais coupés.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Les panneaux Xtream livrent le flux par rafales (~6 s de contenu puis
plusieurs secondes de silence). Le rattrapage mpegts.js réglé à 5 s max /
1 s de marge sautait en avant à chaque rafale puis tombait à sec : mesuré
en prod, 12 sauts et 14 coupures en 30 s sur une chaîne FHD.
Marge portée à 12 s, saut seulement au-delà de 30 s de retard, retard
résorbé par liveSync (x1,1) et purge du buffer déjà lu. Rejoué en prod
avec cette config : 0 coupure en continu.
Ajoute des tests Node du moteur web, lancés par la CI.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Le repli XMLTV journalisait l'URL `xmltv.php?username=…&password=…` du
panneau en clair, et le texte des `ClientException` / `ProcessException`
recopiait aussi l'URL : les identifiants de l'abonné finissaient dans
`docker logs`. Passage par `LogRedactor.redactUrl` dans XmltvEpgService,
EpgApi, le relais live, FFmpegSessionManager et RecordingScheduler.
Test : capture des `print` via une Zone pour vérifier le masquage.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Le serveur n'a qu'une boucle d'événements. La décompression et le
parsing des dumps XMLTV (7000+ chaînes, ~90 Mo) la gelaient pendant
plusieurs secondes : plus rien ne répondait, y compris le relais
turbo.ts. La lecture se coupait donc au démarrage, au moment où
l'écran des chaînes demandait le guide (EPG à 19-26 s dans les logs).
- Téléchargement inchangé, puis gunzip + utf8 + parse dans Isolate.run
- _parse devient statique (aucune capture de this, client HTTP non
transférable)
- Test : la boucle d'événements ne doit pas geler plus de 250 ms
pendant l'indexation d'un dump de taille réelle (1,65 s avant)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
La montée en Dart 3.13 / Flutter 3.47.6 n'avait ajusté que la section
`sdks` à la main. `flutter pub get` réaligne les paquets que le SDK
épingle via flutter / flutter_test : characters 1.4.1, matcher 0.12.20,
material_color_utilities 0.13.0, meta 1.19.0, test_api 0.7.12,
vector_math 2.4.3. bin/pubspec.lock était déjà cohérent.
Vérifié en local (Flutter 3.47.6 / Dart 3.13.5) : dart analyze et
dart test côté serveur (94 tests), dart analyze lib test (infos
seulement) et flutter test côté front.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
L'image ghcr.io/cirruslabs/flutter:stable n'est plus publiée depuis
Flutter 3.44.0 : le build Docker compilait toujours avec Dart 3.12.0
(mai 2026), alors que la CI testait le serveur en Dart 3.13 et le
front en Flutter 3.38.4. Trois SDK différents selon l'endroit.
- Dockerfile : SDK Flutter officiel 3.47.6 (Dart 3.13.5) téléchargé
depuis flutter_infra_release, version en ARG
- CI : Flutter 3.38.4 → 3.47.6, Dart 3.13.2 → 3.13.5 — même SDK que
l'image
- pubspec : contrainte sdk ^3.13.0 (au lieu de >=3.0.0) pour pouvoir
utiliser les nouveautés du langage (constructeurs primaires,
raccourcis `.valeur`, éléments `?x`). Les lockfiles exigeaient déjà
3.10.3 / 3.11.0 : le minimum affiché était faux
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Serveur chargé ou VRAM limite : NVENC refuse d'ouvrir l'encodeur
(OpenEncodeSessionEx failed: out of memory), FFmpeg meurt en une
seconde et la playlist part en 502. Chaque relance retombait sur le
même GPU saturé : le flux restait illisible.
- Serveur : un échec imputable au GPU (NVENC, CUDA, mémoire) relance
la session une fois en libx264, sous le même identifiant — les
segments suivent sans que le lecteur s'en aperçoive. Direct, VOD et
enregistrements
- Le GPU est écarté 2 min après une panne : les sessions suivantes
partent directement sur le CPU au lieu d'échouer une à une
- Source injoignable ou délai dépassé : pas de repli, il doublerait la
charge d'un serveur déjà occupé
- Process.start qui échoue (serveur à court de ressources) : 502/503
au lieu d'une exception remontée en 500
- Client : une erreur réseau fatale avant la première playlist
appelait hls.startLoad(), qui ne relance pas ce chargement → spinner
infini, aucune erreur, relances de page jamais déclenchées. L'erreur
remonte désormais à la page
- mpegts.js : au-delà de 3 erreurs en 60 s, bascule sur HLS au lieu de
recréer sans fin (un FFmpeg neuf toutes les 1,2 s)
- player_lite.html : 3 relances avant l'erreur, comme les autres pages
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
L'hébergeur des picons du fournisseur (51.158.145.100) répond toujours
503 sur toutes ses URL. Couper le flot de requêtes ne suffisait pas : la
grille restait sans logo. Pire, le pixel transparent servi en 200 était
pris pour une image valide par Flutter, qui n'affichait plus son icône
de repli — d'où des tuiles entièrement vides.
- Nouvelle route `/api/logo?src=…&name=…` : URL du panneau d'abord (via
le proxy, mêmes protections anti-SSRF et même mémoire des hôtes
morts), puis recherche par nom dans le dépôt public tv-logo/tv-logos
- Appariement par nom : préfixe pays et suffixes de qualité ignorés,
`+` → `plus`, variante plus longue (`bein-sports-1-french`), puis
marque sans numéro pour les canaux événementiels. Aucune approximation
sur un mot seul
- Liste de logos en cache 24 h (API GitHub : 60 requêtes/h non
authentifiées), 15 min de recul après échec, images en mémoire
- Image indisponible : 410 mis en cache au lieu du pixel. 410 et non
404 : la Cascade du serveur rattrape les 404 et renvoyait celui du
handler statique, sans cache-control
- Client (grille bureau, mobile, enregistrements) : logo toujours
demandé au backend, nom compris, même sans stream_icon. L'onglet
Enregistrements chargeait l'URL http brute, bloquée en HTTPS
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Logos absents : l'hébergeur des picons du fournisseur répond 503 sur
tout, y compris sa racine (serveur en maintenance). Le proxy relayait
ce 503 au navigateur, qui redemandait chaque logo à chaque affichage de
la grille — des centaines de requêtes sortantes mortes par seconde.
- Un statut >= 400 sur une image rend désormais un pixel transparent
avec `Cache-Control: max-age=600`, au lieu de propager l'erreur
- Après trois échecs consécutifs, l'hôte est marqué hors service cinq
minutes : plus aucun appel sortant, réponse servie localement
- Le corps d'erreur amont est drainé, sinon la connexion restait
ouverte jusqu'au timeout
Guide TV très lent (4 à 8 s par chaîne dans les logs) : `player_api`
demande un appel par chaîne, et le panneau met ~4 s à répondre — une
grille de trente chaînes dépassait la minute.
- Le dump `xmltv.php` du panneau passe en premier : une requête couvre
toutes les chaînes, réutilisée trois heures
- L'interrogation chaîne par chaîne reste le repli, puis la source
XMLTV externe
- Une source XMLTV en échec n'est plus retéléchargée à chaque
consultation : quinze minutes de recul
- Index borné à 48 h en avant : un dump national couvre sept jours pour
un millier de chaînes, soit des centaines de Mo pour un guide qui
n'affiche que le programme courant
Lecture saccadée : le relais FFmpeg -> navigateur de `/turbo.ts` ne
transmettait pas la pause du client à la source. Un lecteur plus lent
que le flux laissait FFmpeg produire à pleine vitesse et le serveur
empilait les paquets en mémoire, d'où la dérive derrière le direct. La
contre-pression est maintenant propagée (`pipeWithBackpressure`).
Le serveur tourne sur un seul isolate : le flot de requêtes mortes et
les attentes EPG de plusieurs secondes partageaient la boucle
d'événements avec les paquets vidéo.
Tests ajoutés : cache d'échecs par hôte, contre-pression du relais,
priorité des sources EPG, recul après échec XMLTV, horizon d'index.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The font files I vendored under assets/google_fonts were not fonts. A
valid TTF starts with 00 01 00 00; these started with 80 18 01 00 and
3C A3 00 00. They came from the legacy fonts.googleapis.com endpoint,
and I shipped them without checking the signature.
Skia rejected every one of them, which is what produced the console
errors seen in production:
Failed to parse font family "Karla_regular"
Failed to parse font family "Fraunces_600"
so the whole app rendered in a fallback font instead of Fraunces/Karla.
Removing them restores the working path: google_fonts fetches from
fonts.gstatic.com at runtime, which is what main did before.
This gives up the startup win claimed in 1feeaae. Bundling properly is
still possible but needs static per-weight instances: the only files
google/fonts publishes for these two families are variable fonts, and
google_fonts cannot map a weight onto them in asset mode. A note in
pubspec.yaml records this so the mistake is not repeated.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
getShortEpg ended in `catch (e) { return []; }`, so a 401, a timeout, a
rate-limited panel and a genuinely empty guide all rendered the same
"No Info" label. That made the current problem undiagnosable from the
UI. Failures are now logged with the HTTP status, the Dio error type and
the response shape.
Two concrete causes are handled while we are here:
- Some Xtream panels answer player_api.php with a textual Content-Type.
Dio then hands back a raw String, and `response.data['epg_listings']`
threw, landing in the silent catch. The body is now parsed explicitly
when it arrives as a String.
- The channel grid mounts one EPG provider per visible tile, firing
~32 concurrent requests at player_api.php. Many panels rate-limit well
below that and fail the whole batch. A 3-slot semaphore serialises
them in small groups.
This does not by itself prove why the guide is empty in production; it
makes the reason visible in the browser console.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The gradient Container in the playlist selection and admin screens sized
itself to its content rather than the viewport: under the loose body
constraints, SingleChildScrollView and Wrap both shrink-wrap, so the
gradient was painted only across a 436 px band (48 padding + 340 card +
48 padding) with the rest of the screen falling through to bare black.
BoxConstraints.expand() forces full coverage.
Also:
- both screens now use AppColors.backgroundGradient. The previous ramp
ran surfaceContainerLowest -> background -> baseLevel0, i.e. dark to
lighter to dark, which put a pale band across the middle
- the playlist card was a 3% cream translucent plate, so it took on
whatever sat behind it instead of resting on it; it is now an opaque
surface (surfaceContainerLow, surfaceContainerHigh on hover) with the
lift() shadow at rest and the ember focus halo on hover
- Scaffold backgroundColor pinned to baseLevel0 so route transitions
never flash pure black
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The security hardening in 60d3f42 moved /api/epg, /api/recordings and
/api/season-passes behind authMiddleware, but the Flutter client still
called them through bare package:http. Those requests carry no
Authorization header, and on web BrowserClient sets withCredentials to
false so the session cookie is not sent either — every call came back
401. Symptoms: empty TV guide, empty recordings list, empty season
passes.
Adds AuthedHttp, a thin wrapper that injects the same token ApiClient
and XtreamService already use (localStorage['auth_token']), and routes
the 13 affected calls through it.
subtitle_service is left on plain http: it fetches third-party subtitle
URLs, not our API, and must not leak the session token.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Replaces the Projector Noir palette with "Warm Cinema" and rebuilds the
web players around a shared, event-driven engine.
Design
- app_colors: warm charcoal (#0B0908), cream (#F3E9DF), ember (#D9541F);
adds posterScrim/heroScrim gradients, lift()/emberFocus() shadows,
const cream-alpha tokens replacing Colors.whiteNN, and primaryFill,
a deeper ember reserved for text-bearing fills so white labels clear
the 4.5:1 AA threshold that #D9541F alone does not
- app_theme / mobile_theme: Fraunces (display serif) + Karla (body),
tighter radii, 20+ component themes
- glass_container: opaque layered surfaces replace the blurred glass
- themed_loading_screen restyled; players and boot splash follow
Performance
- drop every BackdropFilter: backdrop blur forced a full framebuffer
read per frame per card, the main source of scroll jank
- bundle Fraunces/Karla as assets so google_fonts stops fetching from
fonts.gstatic.com at startup
- bound image decode size on 12 sites: a 40-poster grid drops from
~240 MB to a few tens of MB of GPU memory
Players (new shared engine: web/xf-player-core.js)
- start on media events instead of polling the buffer every 100-200 ms
- hls.js/mpegts.js loaded on demand, so ~213 KB of mpegts is never
fetched for HLS streams
- liveSyncDurationCount 10 -> 2, startFragPrefetch on, testBandwidth
off, MPEG-TS initial stash 512 KB -> 64 KB
- adaptive escalation fast -> balanced -> safe after repeated stalls in
a 60 s window, applied live for HLS so there is no visible cut
Preserved from the recent streaming work on main: server-side quality
selection, stable player viewId, the turbo direct MPEG-TS path, vendored
library cache headers and the CI/Docker changes. Only the theme layer and
the player front-end were replaced; GlassCard stays in ui_components.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
player_lite (Live TV desktop) ignored set_volume messages from Flutter,
so mute/unmute button had no effect. Added handler matching player.html.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Removed TabBar/TabBarView complexity
- Keep only recordings list display
- Simplified to show only recordings without guide TV and season passes tabs
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- Implement parallel fetching for categories and streams in Live, VOD, and Series
- Add persistent local storage caching for instant UI population
- Optimize JSON parsing logic for large datasets
- Implement failover to local cache when network is unavailable
- Add playback lock to XtreamService to delay EPG during stream init
- Increase MPEG-TS stash buffer to 512KB for better stability
- Enable liveBufferLatencyChasing to prevent stream drift
- Tune HLS parameters for balanced speed and reliability