mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-18 04:39:41 +02:00
Diagnostic : le lecteur remonte ses freezes dans les logs du serveur
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>
This commit is contained in:
1 parent
775caad0ee
commit
a739d6451c
5 files changed
+292
-1
No files matched your search
+118
-1
@@ -190,8 +190,75 @@
|
||||
// ça, une relance après échec laisserait l'ancienne instance réagir aux
|
||||
// commandes du parent et republier des positions périmées.
|
||||
this._listeners = [];
|
||||
|
||||
// Télémétrie des incidents (voir _report) : chaque freeze vécu par
|
||||
// l'utilisateur finit dans les logs du serveur, avec sa cause.
|
||||
this._t0 = Date.now();
|
||||
this._session = Math.random().toString(36).slice(2, 8);
|
||||
var idMatch = /\/api\/(?:live|vod|recordings\/stream)\/([^/?#.]+)/.exec(this.url || '');
|
||||
this._channel = idMatch ? idMatch[1] : '';
|
||||
this._stallSince = null;
|
||||
this._stats = { stalls: 0, stallMs: 0, seeks: 0, errors: 0 };
|
||||
this._lastPos = 0;
|
||||
var self = this;
|
||||
var userOnError = this.onError;
|
||||
this.onError = function (msg) {
|
||||
self._report('error_shown', { detail: String(msg) });
|
||||
userOnError(msg);
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------- Télémétrie ----------------
|
||||
|
||||
/** Secondes d'avance dans le buffer à la position courante. */
|
||||
XFPlayer.prototype._bufferAhead = function () {
|
||||
var v = this.video;
|
||||
try {
|
||||
for (var i = 0; i < v.buffered.length; i++) {
|
||||
if (v.buffered.start(i) <= v.currentTime + 0.5 &&
|
||||
v.buffered.end(i) >= v.currentTime) {
|
||||
return v.buffered.end(i) - v.currentTime;
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
return 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Remonte un incident au serveur (`POST /api/player-log`, écrit dans les
|
||||
* logs `[Player] …`). Jamais bloquant : erreurs ignorées, aucun envoi
|
||||
* sans session.
|
||||
*/
|
||||
XFPlayer.prototype._report = function (event, data) {
|
||||
try {
|
||||
var token = null;
|
||||
try { token = global.localStorage.getItem('auth_token'); } catch (e) {}
|
||||
if (!token || typeof fetch !== 'function') return;
|
||||
var payload = {
|
||||
event: event,
|
||||
session: this._session,
|
||||
ch: this._channel,
|
||||
t: (Date.now() - this._t0) / 1000,
|
||||
mode: this.hls ? 'hls' : (this.mpegts ? 'ts' : 'direct'),
|
||||
profile: this.profile.name,
|
||||
buffer: this._bufferAhead(),
|
||||
pos: this.video.currentTime || 0
|
||||
};
|
||||
for (var k in data) {
|
||||
if (Object.prototype.hasOwnProperty.call(data, k)) payload[k] = data[k];
|
||||
}
|
||||
fetch('/api/player-log', {
|
||||
method: 'POST',
|
||||
keepalive: true,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + token
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
}).catch(function () {});
|
||||
} catch (e) {}
|
||||
};
|
||||
|
||||
/** addEventListener + mémorisation pour un retrait propre. */
|
||||
XFPlayer.prototype._on = function (target, type, handler, options) {
|
||||
target.addEventListener(type, handler, options);
|
||||
@@ -224,6 +291,7 @@
|
||||
var isLive = this.type === 'live';
|
||||
|
||||
this.onLoading('Ouverture du flux');
|
||||
this._report('start', {});
|
||||
|
||||
// Safari / iOS : HLS natif = le chemin le plus rapide, aucune lib à charger.
|
||||
if (isHls && this._canPlayNativeHls()) {
|
||||
@@ -345,6 +413,8 @@
|
||||
hls.on(Hls.Events.ERROR, function (e, d) {
|
||||
if (!d.fatal) return;
|
||||
self.log('Erreur HLS fatale : ' + d.details);
|
||||
self._stats.errors++;
|
||||
self._report('hls_error', { detail: d.type + '/' + d.details });
|
||||
if (d.type === Hls.ErrorTypes.NETWORK_ERROR) {
|
||||
// Playlist jamais obtenue (transcodeur en échec → 502, serveur trop
|
||||
// chargé → délai dépassé), après les relances internes de hls.js.
|
||||
@@ -462,6 +532,7 @@
|
||||
|
||||
self._audioFallbackDone = true;
|
||||
self.log('Aucune piste audio démuxée (codec non supporté) — bascule HLS');
|
||||
self._report('fallback_hls', { detail: 'no_audio' });
|
||||
self.onLoading('Piste audio incompatible — réencodage');
|
||||
|
||||
try {
|
||||
@@ -479,6 +550,8 @@
|
||||
player.on(mpegts.Events.ERROR, function (type, detail) {
|
||||
self.log('Erreur MPEG-TS : ' + type + ' / ' + detail);
|
||||
if (self._destroyed) return;
|
||||
self._stats.errors++;
|
||||
self._report('mpegts_error', { detail: type + '/' + detail });
|
||||
|
||||
// Chaque recréation relance un FFmpeg côté serveur : sur un serveur
|
||||
// déjà saturé, recréer sans fin toutes les 1,2 s ne faisait
|
||||
@@ -503,6 +576,7 @@
|
||||
return;
|
||||
}
|
||||
self.log('Erreurs MPEG-TS répétées — bascule HLS');
|
||||
self._report('fallback_hls', { detail: 'repeated_mpegts_errors' });
|
||||
self.onLoading('Ouverture du flux');
|
||||
self.url = hlsUrl;
|
||||
if (self._canPlayNativeHls()) self._playDirect(hlsUrl);
|
||||
@@ -519,6 +593,7 @@
|
||||
player.detachMediaElement();
|
||||
player.destroy();
|
||||
} catch (e) {}
|
||||
self._report('recreate', {});
|
||||
self._createMpegts(self._mpegtsIsLive);
|
||||
}, 1200);
|
||||
});
|
||||
@@ -585,6 +660,7 @@
|
||||
var next = PROFILES[ORDER[idx + 1]];
|
||||
this.profile = next;
|
||||
this.log('Escalade buffer → ' + next.name + ' (' + reason + ')');
|
||||
this._report('profile', { detail: reason });
|
||||
this.onProfileChange(next.name, reason);
|
||||
|
||||
// HLS : les fenêtres de buffer sont modifiables à chaud, pas besoin de
|
||||
@@ -628,6 +704,19 @@
|
||||
});
|
||||
|
||||
this._on(v, 'playing', function () {
|
||||
if (!self._started) {
|
||||
self._report('first_frame', { ms: Date.now() - self._t0 });
|
||||
}
|
||||
if (self._stallSince !== null) {
|
||||
var stallMs = Date.now() - self._stallSince;
|
||||
self._stallSince = null;
|
||||
// < 250 ms : imperceptible, ne pas noyer les logs.
|
||||
if (stallMs >= 250) {
|
||||
self._stats.stalls++;
|
||||
self._stats.stallMs += stallMs;
|
||||
self._report('stall', { ms: stallMs });
|
||||
}
|
||||
}
|
||||
self._started = true;
|
||||
self.onReady();
|
||||
self.send({ type: 'playback_status', status: 'playing' });
|
||||
@@ -637,9 +726,36 @@
|
||||
self.send({ type: 'playback_status', status: 'paused' });
|
||||
});
|
||||
|
||||
this._on(v, 'waiting', function () { self._noteStall(); });
|
||||
this._on(v, 'waiting', function () {
|
||||
if (self._started && self._stallSince === null) {
|
||||
self._stallSince = Date.now();
|
||||
}
|
||||
self._noteStall();
|
||||
});
|
||||
this._on(v, 'stalled', function () { self._noteStall(); });
|
||||
|
||||
// Sauts (rattrapage du direct, recherche) : d'où, vers où.
|
||||
this._on(v, 'timeupdate', function () {
|
||||
if (!v.seeking) self._lastPos = v.currentTime;
|
||||
});
|
||||
this._on(v, 'seeking', function () {
|
||||
if (!self._started) return;
|
||||
self._stats.seeks++;
|
||||
self._report('seek', { from: self._lastPos, to: v.currentTime });
|
||||
});
|
||||
|
||||
// Bilan toutes les 60 s : nombre et durée cumulée des coupures.
|
||||
this._beatTimer = setInterval(function () {
|
||||
if (!self._started) return;
|
||||
self._report('heartbeat', {
|
||||
stalls: self._stats.stalls,
|
||||
stallMs: self._stats.stallMs,
|
||||
seeks: self._stats.seeks,
|
||||
errors: self._stats.errors,
|
||||
rate: v.playbackRate
|
||||
});
|
||||
}, 60000);
|
||||
|
||||
this._on(v, 'ended', function () {
|
||||
self.send({
|
||||
type: 'playback_ended',
|
||||
@@ -806,6 +922,7 @@
|
||||
XFPlayer.prototype.destroy = function () {
|
||||
this._destroyed = true;
|
||||
clearInterval(this._posTimer);
|
||||
clearInterval(this._beatTimer);
|
||||
this._listeners.forEach(function (l) {
|
||||
try { l.target.removeEventListener(l.type, l.handler); } catch (e) {}
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user