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>
Certains conteneurs ou horodatages ne se prêtent pas à `-c:v copy`.
Plutôt qu'un écran d'erreur définitif là où le ré-encodage fonctionnait
jusqu'ici, la session est relancée une fois sans copie quand la première
tentative ne produit pas de playlist.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T1A6WRnoktWcT7ZG4b2rbC
La lecture d'un enregistrement démarrait au milieu du programme et se
coupait aussitôt ; il fallait relancer une deuxième fois pour qu'elle
tienne.
Cause principale, introduite avec le passage de la playlist en `EVENT` :
tant que le transcodage n'est pas terminé, la playlist n'a pas
d'`EXT-X-ENDLIST`, et hls.js traite alors tout contenu comme du direct.
Sans `startPosition` explicite il démarre au « bord du direct » — donc
collé au front d'encodage, sans la moindre avance de segments. D'où un
départ en plein milieu, puis une coupure immédiate. Au second essai, le
transcodage était terminé (ENDLIST présent, contenu traité en VOD) et
tout se passait bien.
Hors direct : `startPosition: 0` (ou la position de reprise), rattrapage
de latence neutralisé (il accélérait la lecture puis forçait un saut en
avant vers un direct inexistant), et délai d'attente de playlist porté de
10 s à 45 s — une session FFmpeg qui démarre n'est plus prise pour un
manifeste mort.
Cause de fond : la lecture ré-encodait la vidéo. Or la capture se fait en
`-c copy`, donc le fichier contient le codec de la chaîne, presque
toujours du H.264, directement lisible en HLS. Le ré-encodage tenait à
peine le temps réel en 1080p, ce qui laissait le lecteur courir après
l'encodeur en permanence. La vidéo est désormais copiée telle quelle
quand elle est en H.264 (seul l'audio est converti en AAC) : la
segmentation va à la vitesse du disque, l'enregistrement devient
navigable en quelques secondes et les sauts ne redémarrent presque plus
jamais FFmpeg. Les codecs que le navigateur ne sait pas lire (HEVC,
MPEG-2…) restent ré-encodés.
En complément :
- le serveur attend trois segments d'avance avant de servir la playlist,
et sert ce qu'il a plutôt qu'une erreur si le délai expire ;
- le lecteur de bureau réessaie seul jusqu'à 3 fois, comme le lecteur
mobile le faisait déjà ;
- `XFPlayer.destroy()` retire ses écouteurs : sans ça, une relance
laissait l'instance précédente réagir aux commandes du parent et
republier des positions périmées ;
- les appels ffprobe passent par `MediaProbe`, qui mémorise durée et
codec tant que le fichier ne change pas.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T1A6WRnoktWcT7ZG4b2rbC
La lecture d'un enregistrement ne permettait ni de reculer, ni d'avancer,
ni de reprendre où on s'était arrêté.
Cause côté serveur : la playlist était servie en
`EXT-X-PLAYLIST-TYPE:VOD`. hls.js la considère alors comme définitive et
ne la relit jamais — il ne voyait donc que les quelques secondes déjà
transcodées à l'ouverture, d'où une durée absurde et une barre de
progression inutilisable. Elle passe en `EVENT` : la zone navigable
grandit au rythme de l'encodage.
FFmpeg transcodant séquentiellement, sauter à 45 min imposait malgré tout
d'attendre qu'il y arrive. La playlist accepte donc `?start=<secondes>` et
démarre une session dédiée (`-ss` avant `-i`). Les segments y sont
préfixés par l'offset (`t2700/segment_000.ts`) : la query string ne
survit pas aux URLs relatives de la playlist, deux sessions du même
enregistrement se seraient mélangées. Les sessions d'offsets abandonnés
sont fermées dès qu'aucun lecteur ne les consomme (killIdleSiblings).
Côté lecteur, tout ce qui sort du moteur est désormais en temps absolu
(`offset` réinjecté) : une recherche dans la zone déjà transcodée est un
simple `currentTime`, au-delà le lecteur répond `seek_out_of_range` et
l'app relance le flux à cet instant.
Ajouts visibles :
- reprise proposée à l'ouverture d'un enregistrement entamé, et mention
« Reprendre à … » avec sa progression dans la liste ;
- durée réelle mesurée par ffprobe (`duration_seconds`), mise en cache
tant que le fichier ne bouge pas — `end_time - start_time` n'est que la
durée programmée ;
- sauts ±10 s et ±1 min, sélecteur de vitesse (0,5× à 2×), portion déjà
transcodée visible sur la barre ;
- raccourcis K/espace, J/L, Maj+←/→, 0-9, F, M, virgule/point.
L'identifiant de vue de plateforme porte maintenant un numéro de
génération : sans lui, réutiliser le même identifiant laissait l'ancienne
iframe en place (`registerViewFactory` conserve la première fabrique
enregistrée) — ce qui affectait déjà le retour sur une chaîne
précédemment zappée.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T1A6WRnoktWcT7ZG4b2rbC
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FSovdudi3KBwZK3aC42fw
Son manquant sur certaines chaînes :
- Nouvelle route GET /api/live/<id>/turbo.ts pour le player web : vidéo
copiée (-c:v copy), audio TOUJOURS réencodé en AAC. mpegts.js ne démuxe
que l'AAC/MP3 : les chaînes en AC-3/E-AC-3/MP2 passaient par le proxy
brut → image sans son, et le fallback HLS côté client ne couvrait pas
les codecs démuxés mais non décodables par le navigateur (MP2)
- getLiveStreamUrlTs pointe sur la route turbo ; le proxy brut
/api/live/<id>.ts reste inchangé pour le scheduler d'enregistrement
- FFmpeg turbo tué dès que le client zappe (onCancel) ; stderr redacté
- Le fallback client (_hlsEquivalent) reconnaît le nouveau format d'URL
et reste en filet de sécurité
Temps de chargement :
- Players vendorisés mis à jour : hls.js 1.6.7 → 1.7.1,
mpegts.js 1.7.3 → 1.8.2
- Live HLS et turbo : -fflags nobuffer + probesize/analyzeduration 1 Mo
(sans borne, FFmpeg pouvait sonder plusieurs secondes avant le premier
segment)
- VOD : probesize 10 Mo → 5 Mo, analyzeduration 5 s → 2 s
- Live 'high' : preset medium → veryfast (zerolatency déjà actif) ;
lecture d'enregistrement : medium/crf18 → veryfast/crf20 (medium ne
tenait pas le temps réel en 1080p sur CPU modeste)
- waitForPlaylist : polling 500 ms → 100 ms
- Suppression du cache-buster &v=timestamp qui re-téléchargeait
player.html à chaque zap ; les .html sont servis en no-cache côté
serveur pour garder la fraîcheur après déploiement
- mpegts.js : liveBufferLatencyChasing activé en profil « fast » — les
micro-coupures ne font plus dériver la lecture derrière le direct
Validé : dart analyze (0 issue) + dart test (48/48) sur bin/, syntaxe JS
vérifiée.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015oEu9QayWsw7hCKhenxgVa
Lockfiles :
- pubspec.lock et bin/pubspec.lock versionnés (le .gitignore les excluait
via *.lock ; le Dockerfile résolvait des versions fraîches à chaque
build, comme son commentaire l'assumait) ; le Dockerfile les COPY
désormais, .dockerignore ajusté
CI (ci.yml) :
- flutter-version 3.38.4 et Dart 3.13.2 épinglés (channel: stable seul
fait dériver le SDK et peut casser la CI sans changement du dépôt)
- cache pub activé, concurrency: cancel-in-progress
Publication (docker-publish.yml) :
- Chaîné sur la CI via workflow_run : l'image :latest n'est publiée
qu'après analyze/test/build verts sur main (l'ancien push: [main]
tournait en parallèle et pouvait publier une image cassée) ; build du
head_sha validé par la CI
docker-compose :
- Défaut RECORDINGS_PATH: ./data/recordings au lieu du chemin unRAID
spécifique à une machine (/mnt/user/Data/Sport)
- Variable TZ (défaut Europe/Paris)
Docs :
- README réécrit : il décrivait une architecture disparue (Hive/
IndexedDB, hachage SHA-256, dhttpd port 8080, --web-renderer html)
au lieu de l'état réel (SQLite serveur, bcrypt, binaire natif port
8089, enregistrements, transcodage, CI)
- DEPLOYMENT_CHECKLIST.md archivé (6 fichiers cités inexistants,
versions de dépendances fausses)
- docs/archive/README.md : avertissement que ces documents sont
historiques (plusieurs se déclarent « COMPLETE » à tort)
Non traité ici : l'épinglage de la release FFmpeg du Dockerfile (l'accès
aux releases BtbN est bloqué depuis cet environnement, impossible de
vérifier un tag valide) et la suppression du code mort (reportée après le
merge des PRs #7/#8/#9 pour éviter les conflits).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015oEu9QayWsw7hCKhenxgVa
Onglet Enregistrements :
- TabBar à 3 vues : Guide TV (programmer depuis l'EPG), liste des
enregistrements, Season Passes. _EpgGuideView et _SeasonPassesView
(~800 lignes fonctionnelles) n'étaient plus instanciés depuis une
refonte — programmer depuis le guide et les enregistrements
récurrents étaient devenus inaccessibles
- Confirmation avant suppression d'un enregistrement (la corbeille
supprimait immédiatement, sans retour ni annulation)
- Erreur de chargement avec bouton Réessayer
Favoris :
- Bouton cœur sur les tuiles chaînes desktop (overlay) et mobile :
toggleFavorite() n'était appelé nulle part, le filtre « Favoris »
affichait toujours une liste vide
Reprise de lecture :
- Films et épisodes lisent playbackPositionsProvider et passent
startTime au player : les positions étaient écrites mais jamais
relues, tout repartait de zéro
- Plus de sauvegarde de position en live (une entrée bidon par chaîne
zappée)
Mobile :
- 5e onglet REC réutilisant RecordingsTab
- IndexedStack au lieu du switch : les onglets conservent scroll et
catalogues chargés (aligné sur le dashboard desktop)
Dashboard :
- Menu profil sur l'avatar de la sidebar (nom d'utilisateur +
déconnexion) : le bouton était mort, aucune déconnexion possible
depuis le dashboard desktop
- Erreur de chargement des chaînes avec bouton Réessayer
Validé : flutter analyze (202 issues, identique à la baseline main,
0 erreur/warning), flutter test (OK), flutter build web --release (OK).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015oEu9QayWsw7hCKhenxgVa
Proxy /api/xtream :
- Authentification de session rétablie (Authorization ou cookie HttpOnly
session — les requêtes navigateur même-origine le portent) ; le proxy
était volontairement ouvert, offrant un rebond SSRF non authentifié
- Redirections suivies manuellement avec revalidation à chaque saut
(hôte privé interdit + allowlist de domaine) : avec followRedirects,
seule l'URL initiale était validée, une 302 amont suffisait pour
atteindre un hôte interne
- Erreurs proxy sans détail d'exception (ClientException porte l'URL
amont, credentials Xtream inclus), logs redactés
Logs :
- redactedLogRequests remplace logRequests() de shelf : l'URI de
/api/xtream/<url> écrivait username/password Xtream en clair à chaque
requête, annulant l'effort de LogRedactor partout ailleurs
- Les 500 d'epg_api ne renvoient plus e.toString() au client (même
risque ClientException) ; détail redacté en log serveur
Middleware :
- X-Forwarded-For honoré uniquement depuis un proxy de confiance
(loopback + RFC1918 par défaut, surchargables via TRUSTED_PROXIES) :
un client direct forgeait l'en-tête et contournait le rate limit
global comme la limite de tentatives de login
- Honeypot comparé sur chemin exact/préfixe : l'ancien
contains(trap.replaceAll('/','')) bloquait toute URL contenant
console, env ou wpadmin, y compris des URLs proxifiées légitimes
Comptes :
- Mot de passe admin initial aléatoire (Random.secure, affiché une fois
au démarrage) ou ADMIN_INITIAL_PASSWORD ; fini le admin/admin persistant
- Longueur minimale de 8 caractères à la création et au changement
Divers :
- CleanupService ne cible plus Directory.systemTemp en récursif (il
supprimait les temporaires de la VM Dart et le parent des sessions HLS)
- web/xf-player-core.js : postMessage vers location.origin au lieu de
'*', et filtrage d'event.origin à la réception (le côté Flutter le
faisait déjà)
Validé : dart analyze (0 issue) et dart test (48/48) sur bin/.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015oEu9QayWsw7hCKhenxgVa
Fuseaux horaires :
- POST /api/recordings rejette en 400 les dates sans fuseau et normalise
tout en UTC à l'écriture (l'interprétation des dates naïves dans le TZ
du conteneur décalait les enregistrements de 1-2 h)
- Helper frontend unique postRecording() : les 2 points de création
(modal, guide EPG) envoient la même convention UTC
Contrôle d'accès :
- stop/delete/logs d'un enregistrement et delete d'un season pass
vérifient la propriété (userId ou admin), comme playlists_handler
- Suppression des replis 'dev_user_id' et 'admin' (401 sans session)
SQLite :
- PRAGMA foreign_keys/WAL/busy_timeout (les ON DELETE CASCADE déclarés
ne s'appliquaient pas : sessions et playlists orphelines)
- Migrations de schéma versionnées (schema_version) + index user_id,
start_time, season_passes(user_id)
Gestion disque :
- Refus explicite de capture sous MIN_FREE_DISK_MB (défaut 500 Mo)
- Rotation par quota d'octets (RECORDINGS_QUOTA_GB, opt-in) qui ne touche
jamais un enregistrement actif et supprime fichiers + ligne ensemble ;
l'ancienne rotation « 50 fichiers » pouvait effacer une capture en cours
- DELETE /api/recordings/<id> supprime aussi .mkv/.log/parties (SafePath)
Scheduler :
- Arrêt gracieux orchestré par server.dart : clôture des enregistrements
(fusion des parties, statut) avant killAll des sessions de streaming ;
l'ancien handler SIGTERM de FfmpegSessionManager faisait exit(0) direct
- Noms de fichiers uniques par fragment d'id (deux enregistrements du
même programme s'écrasaient mutuellement avec -y)
- Requête filtrée scheduled/recording au lieu de toute la table / 10 s
- Statut cancelled pour un scheduled arrêté (completed sans fichier
cassait la lecture)
Season passes :
- Playlist du propriétaire du pass résolue à chaque scan (l'injection
figée du 1er utilisateur rendait les passes muets après ajout de
playlist, et mélangeait les credentials en multi-utilisateurs)
- Correspondance exacte par défaut (match_mode, migration en 'contains'
pour l'existant), plafond de créations par scan, réalignement des
horaires déplacés dans l'EPG, déduplication tolérante ±2 min
- Redaction des erreurs de scan (ClientException contient l'URL amont)
API de suivi (polling conservé) :
- GET /api/recordings enrichi : progress_pct, file_size_bytes,
retry_count, is_active ; barre de progression + taille dans la liste
Validé : dart analyze (0 issue) et dart test (48/48) sur bin/.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015oEu9QayWsw7hCKhenxgVa
- Nouveau bus de notification (recordingsRefreshBus) : la liste se recharge
immédiatement dès qu'un enregistrement est créé, planifié ou arrêté depuis
n'importe où dans l'app (guide EPG, modal, widget rapide)
- Polling en arrière-plan : 5 s quand un enregistrement est en cours ou
planifié, 20 s sinon, pour suivre les changements de statut sans clic
- Rafraîchissement silencieux (pas de spinner ni de vidage de liste) pour
éviter le clignotement ; les erreurs transitoires ne remplacent pas la liste
- Indicateur « Suivi auto » avec l'heure de dernière actualisation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015oEu9QayWsw7hCKhenxgVa
Chaque enregistrement arrivé au bout de sa fenêtre était marqué
« Échoué / Interruption inattendue du serveur » alors que le fichier était
bien sur le disque : le tick lisait la base AVANT d'arrêter les captures
terminées, si bien que l'instantané annonçait encore « recording » pour un
enregistrement déjà retiré de la table des processus actifs — la détection
d'orphelin le requalifiait aussitôt en échec. La base est désormais lue
après les arrêts, et la requalification revérifie le statut courant.
Deux autres façons de perdre un enregistrement sont corrigées au passage :
- FFmpeg livre encore des morceaux de stderr après la résolution de
`exitCode` ; écrire sur l'`IOSink` déjà fermé levait une `StateError`
depuis un callback de stream, donc une erreur asynchrone non rattrapée
qui tue l'isolate — et avec lui le serveur et toutes les captures en
cours. Le log passe par un écrivain tolérant et n'est fermé qu'une fois
stdout et stderr drainés.
- Une coupure amont terminait la capture définitivement. FFmpeg reçoit
maintenant les options de reconnexion (comme le proxy live), et le
planificateur relance la capture sur la fin de fenêtre quand le process
sort trop tôt (backoff 3→30 s, quota remis à zéro après une capture
saine). Les parties issues des relances sont recollées dans le fichier
principal via le demuxer `concat`, la lecture reste donc un seul fichier.
Également :
- reprise des captures interrompues par un redémarrage du conteneur tant
que la fenêtre est ouverte, au lieu d'un échec sec ; fichier partiel
conservé (statut « terminé ») quand la fenêtre est passée
- `-t` calculé sur le temps restant jusqu'à la fin programmée : un
démarrage tardif ne rogne plus la fin du programme
- `-hide_banner -nostats` : le log d'enregistrement redevient lisible (et
ne pèse plus des mégaoctets, il est relu en entier par l'API)
- `RECORDINGS_DIR` et `FFMPEG_PATH` surchargeables, et le motif d'erreur
est effacé au (re)démarrage d'une capture
Vérifié avec un faux ffmpeg sur un planificateur réel : avant correctif,
un enregistrement mené jusqu'à la fin de fenêtre ressort « failed /
Interruption inattendue du serveur » ; après, « completed » — de même que
la reprise après interruption, la relance après coupure et la fusion des
parties.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E5xWYTbtZwJuYB3E51K243
Les journaux du serveur montrent la grille Live TV se faire couper en 429 :
"Rate limit exceeded", une trentaine de requêtes refusées d'affilée après
quelques écrans de défilement.
Deux causes cumulées. D'une part le passage du guide de Dio vers AuthedHttp a
fait perdre le cache de réponses qui existait auparavant : chaque
reconstruction de tuile repartait en requête. Un cache mémoire de dix minutes
le rétablit — le backend garde déjà ces réponses trente minutes, il n'y a
aucune raison d'aller les rechercher.
D'autre part le plafond de deux cents requêtes par minute et par adresse était
trop bas pour l'usage réel : une grille demande un logo et un guide par tuile,
et tous les clients partagent l'adresse du reverse proxy. Il passe à six
cents.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mpegts.js ne démuxe que l'AAC et le MP3. Une chaîne diffusée en E-AC3
(stream_type 0x87) ou en AC-3 (0x81) était donc lue sans aucune piste audio,
sans erreur ni avertissement : l'image passait, le son était simplement absent.
Les journaux le montrent au PMT — "pid_stream_type":{"256":27,"257":135} avec
un "common_pids" réduit au seul h264, là où les flux audibles annoncent
"257":15 pour l'AAC.
L'évènement MEDIA_INFO porte l'information : quand il arrive sans piste audio,
le lecteur détruit la session MPEG-TS et rejoue la même chaîne par
/api/live/<id>/source/playlist.m3u8. Le préréglage source garde la vidéo en
copie de flux et ne réencode que l'audio, en AAC — le coût processeur reste
négligeable. La bascule n'est tentée qu'une fois par lecture, et seulement sur
une URL live directe.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le guide du fournisseur est figé sur beaucoup de revendeurs : sur le compte de
test, get_simple_data_table répond correctement mais le dernier programme
français date de deux jours, et le xmltv.php du panneau ne couvre aucune chaîne
française. Aucun correctif côté appel Xtream ne pouvait donc remplir la grille.
Nouveau service XmltvEpgService : téléchargement de dumps XMLTV publics,
décompression gzip détectée sur le nombre magique, parsing en flux via
XmlEventReader — un dump national fait 46 Mo décompressés, en charger l'arbre
DOM coûterait plusieurs centaines de mégaoctets dans le conteneur. Mesuré à
environ 1 s pour 973 chaînes. Index rafraîchi toutes les 6 h, programmes
terminés depuis plus de 6 h écartés à l'indexation.
L'appel sortant reste un dernier recours. L'ordre est inchangé —
get_simple_data_table, puis get_short_epg — et le dump n'est consulté que si
le panneau ne rend aucun programme couvrant l'instant présent : compter les
entrées ne suffisait pas, un guide périmé en renvoie des centaines. La source
se configure par EPG_XMLTV_URLS ; vider la variable supprime tout appel
sortant. La réponse porte un en-tête X-Epg-Source pour savoir qui a répondu.
Les identifiants sont rapprochés après normalisation : le dump écrit
France.2.fr là où le panneau annonce France2.fr. Repli sur le nom affiché de
la chaîne, et table stream_id vers identifiant EPG mise en cache 6 h.
Le client passe désormais par /api/epg au lieu d'appeler player_api.php en
direct. Il dupliquait le choix de l'action, la conversion des fuseaux et le
décodage base64 ; le backend le fait une fois, met en cache pour tous les
utilisateurs, et sait basculer sur le repli — bascule impossible depuis le
navigateur.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
La détection lisait `webkitFullscreenElement` et `mozFullScreenElement` via un
cast `dynamic`. dart2js ne retombe pas sur la propriété JavaScript quand le
membre n'est pas déclaré sur la classe `Document` : il lève un
`NoSuchMethodError`, visible en console sous la forme
`method not found: 'gaR5'`. Le test d'état échouait donc à chaque appel, avant
même de pouvoir demander le plein écran.
Seule l'API non préfixée est désormais utilisée ; elle est supportée par tous
les navigateurs visés.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Plein écran — le bouton n'existait que dans les contrôles VOD, sous une icône
aspect_ratio peu parlante, et les contrôles Live TV n'en avaient aucun. Ajout
du bouton côté Live TV et passage aux icônes fullscreen / fullscreen_exit.
L'état suit désormais l'évènement fullscreenchange du document : sortir par la
touche Échap laissait l'icône désynchronisée.
Proxy live — la route /api/live/<id>.ts relayait la source sans aucune reprise.
Les panneaux Xtream ferment régulièrement la connexion en cours de route, et le
MediaSource du navigateur recevait alors un sourceEnded : la lecture s'arrêtait
net au bout de quelques dizaines de secondes. Le proxy rouvre maintenant la
source tant que le client écoute, avec quatre tentatives et une seconde
d'attente entre chaque, le temps que le panneau libère le slot de connexion.
Le compteur repart de zéro dès qu'une connexion a tenu plus d'une minute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
41 couleurs Material brutes traînaient encore dans l'interface, hors palette :
rouge, vert, orange et surtout un violet omniprésent sur les Season Pass, qui
n'appartient à aucune famille du thème.
Correspondances retenues :
- Colors.red / redAccent -> AppColors.live (#E5484D) pour les badges LIVE et
les indicateurs d'enregistrement, AppColors.error pour les messages d'erreur
- Colors.green / greenAccent -> AppColors.success
- Colors.orangeAccent -> AppColors.warning
- Colors.amber -> AppColors.ratingGold, pour aligner les étoiles de notation
du mobile sur celles du bureau
- Colors.purpleAccent -> famille secondary (terre cuite)
- fonds de boutons passés sur les variantes *Container avec leur couleur de
premier plan, le remplissage clair ne portant pas de texte lisible
Le titre du programme EPG dans la grille Live TV utilisait ratingGold, un jeton
réservé aux notes : il passe sur primary, l'accent ember du thème.
Les fonds de SnackBar vert/rouge deviennent surfaceContainerHigh, le succès et
l'échec restant portés par le pictogramme du message.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
EPG — les trois chemins (backend /api/epg, XtreamService.getShortEpg et
getShortEPG) n'utilisaient que get_epg et get_short_epg. Beaucoup de
panneaux Xtream ne connaissent pas get_epg : ils répondent 200 avec le
bloc d'authentification, sans epg_listings, ce qui produisait un guide
vide indiscernable d'une chaîne sans programme. get_short_epg y renvoie
epg_listings vide en permanence.
- backend : get_simple_data_table en premier, get_short_epg en repli
- client : bascule automatique et mémorisée vers le tableau complet dès
qu'un panneau est muet sur l'action légère, pour ne pas doubler les
requêtes sur chaque tuile de grille
- dates dérivées des *_timestamp epoch et émises en ISO-8601 UTC : les
champs texte sont dans le fuseau du panneau, sans indicateur de zone,
et étaient relus comme de l'heure locale — le guide était décalé et
les enregistrements planifiés depuis le guide l'étaient aussi
- cache backend : la clé incluait seulement le channelId, deux playlists
partageant un stream_id se servaient mutuellement leur guide
UI — les deux halos d'ambiance du dashboard étaient restés de l'ancien
thème : primary (#FFB68C) à 40 % posé sur le coin haut-gauche, donc pile
derrière la sidebar, et info (#A3B8C4, bleu-gris) à 35 % dans une palette
entièrement chaude. Ramenés à des braises ember à 8 % / 6 %.
- fond via AppColors.backgroundGradient
- onglet actif rempli en primaryContainer (surface) et non primary
(teinte claire réservée au texte/icônes)
- sidebar en niveau flottant : en niveau 1 son fond #181310 se confondait
avec le haut du dégradé #1A1310
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>
Full visual refonte replacing the generic neon-blue glass look:
- palette: warm film-black surface ladder, tungsten amber primary,
warm taupe secondary, verdigris teal tertiary; semantic and
category colors retuned to match (coral live, gold movies)
- typography: Syne (display/headlines) + Instrument Sans (body/UI)
replace Space Grotesk/Inter/Outfit across all screens
- glassmorphism tokens warmed (amber inner glow, warm-white borders)
- web player theme.css + index.html loader mirror the new tokens
- hardcoded Colors.blue* in streaming settings and recordings tabs
now use AppColors tokens
- DESIGN_SYSTEM.md updated to v3.0 Projector Noir
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- reaper no longer kills exited sessions immediately: a finished VOD
transcode was reaped (segments deleted) while still being watched
- reuse completed VOD/recording sessions instead of re-transcoding
- live input: add -rw_timeout 30s and -reconnect_at_eof so a stalled
upstream triggers reconnect instead of wedging ffmpeg forever
- reaper watchdog restarts live sessions whose playlist stopped updating
- waitForPlaylist fails fast on clean ffmpeg exit without output
- direct .ts proxy: close http.Client on stream end/error (socket leak)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Stop sending no-store on vendored player libraries: hls.min.js and
mpegts.min.js (~750 KB) were re-downloaded on every player open
- hls.js: start with 1 live segment instead of 3, lower buffer-first
threshold 1.5s -> 0.8s, poll buffer every 100ms
- mpegts.js: initial stash 512KB -> 128KB, start at 0.5s buffered
- Live FFmpeg: -hls_init_time 1 closes the first segment after ~1s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
.dockerignore excluded entrypoint.sh while the Dockerfile COPYs it into
the runtime stage; buildx (docker-container driver) fails hard on this
where the legacy builder only warned.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
streamAuthMiddleware wrapped the whole streaming router inside the
Cascade, so any unmatched path without a session (e.g. GET / through the
reverse proxy) returned 401 before reaching the static file handler.
Now only /api/live, /api/vod and /api/recordings/stream are guarded;
other paths fall through to the router's 404 and the Cascade continues.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Builds and pushes ghcr.io/r0m1k3/xtremflow:latest (+ sha tag) so update
managers (Unraid/Watchtower/Portainer) can detect new images instead of
relying on a local build context.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The frontend CI job ran `flutter analyze` from the repo root, which also
analyzed the bin/ server package without its dependencies resolved
(shelf_router, sqlite3, bcrypt, test), producing hundreds of
uri_does_not_exist errors. bin/ is a standalone package covered by the
backend job, so it is now excluded from root analysis.
Also:
- Remove all unused fields/variables flagged as analyzer warnings
(api_client, cache_service, player_screen, subtitle_service,
live_tv_tab, mobile screens)
- Run `flutter analyze --no-fatal-infos` in CI: pre-existing deprecation
infos (withOpacity, dart:html) stay non-fatal while errors and warnings
still fail the build
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>