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
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
Add `dart run build_runner build --delete-conflicting-outputs` step before Flutter build to ensure all generated files (Riverpod and Hive) are fresh and properly generated. This ensures no stale or incompatible generated code causes build failures.
Also update CACHEBUST to v4 to force Docker to bypass cache and apply all recent fixes.
Add `dart run build_runner build --delete-conflicting-outputs` step before Flutter build to ensure all generated files (Riverpod and Hive) are fresh and properly generated. This ensures no stale or incompatible generated code causes build failures.
Also update CACHEBUST to v4 to force Docker to bypass cache and apply all recent fixes.
Update CACHEBUST arg to 2025-12-08-v3 to ensure Docker doesn't use cached layers and rebuilds with the latest code fixes. This ensures the duplicate height line fix is included in the build.
Update CACHEBUST arg to 2025-12-08-v3 to ensure Docker doesn't use cached layers and rebuilds with the latest code fixes. This ensures the duplicate height line fix is included in the build.
Remove duplicate ..style.height = '100%' line that was left over from merge conflict resolution. This duplicate assignment may have been causing Flutter build compilation issues.
Remove duplicate ..style.height = '100%' line that was left over from merge conflict resolution. This duplicate assignment may have been causing Flutter build compilation issues.