mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-11 17:30:00 +02:00
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>
71 lines
2.4 KiB
Dart
71 lines
2.4 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:shelf/shelf.dart';
|
|
|
|
/// Journal des incidents de lecture remontés par le lecteur web.
|
|
///
|
|
/// POURQUOI : les freezes (image et son figés puis reprise) se produisent
|
|
/// chez l'utilisateur, dans son navigateur ; le serveur n'en voit rien. Le
|
|
/// lecteur envoie ici chaque coupure, erreur, recréation et saut, et le
|
|
/// serveur les écrit dans `docker logs` (`[Player] …`) : on sait alors si un
|
|
/// freeze vient d'un buffer à sec, d'une erreur du démuxeur ou d'une
|
|
/// relance du flux.
|
|
///
|
|
/// Entrée non fiable : seuls des champs connus, courts et typés passent.
|
|
|
|
const _events = {
|
|
'start', 'first_frame', 'stall', 'seek', 'mpegts_error', 'recreate',
|
|
'fallback_hls', 'hls_error', 'profile', 'error_shown', 'heartbeat',
|
|
};
|
|
|
|
const _numericFields = {
|
|
't', 'ms', 'buffer', 'pos', 'from', 'to', 'stalls', 'stallMs', 'seeks',
|
|
'errors', 'rate',
|
|
};
|
|
|
|
const _textFields = {'session', 'ch', 'mode', 'profile', 'detail'};
|
|
|
|
/// Ligne de journal pour l'événement [raw], ou null s'il est invalide.
|
|
String? formatPlayerEvent(Object? raw) {
|
|
if (raw is! Map) return null;
|
|
final event = raw['event'];
|
|
if (event is! String || !_events.contains(event)) return null;
|
|
|
|
final parts = <String>[event];
|
|
for (final key in _textFields) {
|
|
final value = raw[key];
|
|
if (value is String && value.isNotEmpty) {
|
|
// Ni espace, ni retour ligne, ni URL : jamais d'injection de faux
|
|
// journaux, jamais d'identifiants recopiés depuis une URL d'erreur.
|
|
final safe = value
|
|
.replaceAll(RegExp(r'https?://\S+'), '<url>')
|
|
.replaceAll(RegExp(r'[^\w.\-:/<>]'), '_');
|
|
parts.add('$key=${safe.length > 60 ? safe.substring(0, 60) : safe}');
|
|
}
|
|
}
|
|
for (final key in _numericFields) {
|
|
final value = raw[key];
|
|
if (value is num && value.isFinite) {
|
|
parts.add('$key=${value is int ? value : value.toStringAsFixed(1)}');
|
|
}
|
|
}
|
|
return '[Player] ${parts.join(' ')}';
|
|
}
|
|
|
|
Future<Response> handlePlayerLog(Request request) async {
|
|
final body = await request.readAsString();
|
|
if (body.length > 4096) return Response(413);
|
|
Object? decoded;
|
|
try {
|
|
decoded = jsonDecode(body);
|
|
} catch (_) {
|
|
return Response.badRequest();
|
|
}
|
|
final events = decoded is List ? decoded.take(20) : [decoded];
|
|
for (final event in events) {
|
|
final line = formatPlayerEvent(event);
|
|
if (line != null) print(line);
|
|
}
|
|
return Response(204);
|
|
}
|