mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-11 17:30:00 +02:00
Lecture TV : reconstituer la réserve en lisant à x0,97
Télémétrie (session olfp14) : le panneau n'avait envoyé que ~18 s d'avance au démarrage ; la réserve est restée à 13-20 s et un silence du panneau a fini en coupure de 5,5 s. Le lecteur ne fabriquait pas de réserve : il gardait seulement celle offerte par le panneau. Régulateur de vitesse unique pour le direct MPEG-TS, chaque seconde : sous 20 s d'avance, x0,97 (hauteur du son préservée) jusqu'à 25 s ; au-delà de 35 s (rafale de rattrapage), x1,1 jusqu'à 28 s. Seuils d'entrée/sortie distincts : pas d'oscillation. liveSync de mpegts.js désactivé : il ne sait qu'accélérer et remettait la vitesse à 1 sous sa cible, ce qui annulerait le ralenti. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6733f80ffd
commit
f82c70ae05
2 files changed
+139
-18
No files matched your search
@@ -93,12 +93,32 @@ for (const profile of ['fast', 'balanced', 'safe']) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
test('live : le retard est résorbé en douceur (liveSync) plutôt que par sauts', () => {
|
/** Réglages du régulateur de vitesse (exposés par le moteur). */
|
||||||
|
function liveTuning() {
|
||||||
|
const window = {
|
||||||
|
addEventListener() {}, removeEventListener() {},
|
||||||
|
console: { log() {} },
|
||||||
|
location: { origin: 'https://xf.test', href: 'https://xf.test/', search: '' },
|
||||||
|
parent: { postMessage() {} },
|
||||||
|
};
|
||||||
|
vm.runInContext(source, vm.createContext({
|
||||||
|
window, location: window.location,
|
||||||
|
document: { addEventListener() {}, removeEventListener() {} },
|
||||||
|
URLSearchParams, setInterval: () => 0, clearInterval() {}, setTimeout,
|
||||||
|
}));
|
||||||
|
return window.XFPlayerLiveTuning;
|
||||||
|
}
|
||||||
|
|
||||||
|
test('live : un seul régulateur de vitesse (liveSync de mpegts.js désactivé)', () => {
|
||||||
|
// liveSync ne sait qu'accélérer et remet la vitesse à 1 sous sa cible :
|
||||||
|
// il annulerait le ralenti qui reconstitue la réserve.
|
||||||
const c = liveMpegtsConfig();
|
const c = liveMpegtsConfig();
|
||||||
assert.equal(c.liveSync, true);
|
assert.notEqual(c.liveSync, true);
|
||||||
assert.ok(c.liveSyncPlaybackRate > 1 && c.liveSyncPlaybackRate <= 1.2);
|
const t = liveTuning();
|
||||||
assert.ok(c.liveSyncTargetLatency < c.liveSyncMaxLatency);
|
assert.ok(t.slowRate < 1 && t.slowRate >= 0.95, 'ralenti audible au-delà');
|
||||||
assert.ok(c.liveSyncMaxLatency <= c.liveBufferLatencyMaxLatency);
|
assert.ok(t.fastRate > 1 && t.fastRate <= 1.15);
|
||||||
|
assert.ok(t.lowAhead < t.targetAhead && t.targetAhead < t.highAhead);
|
||||||
|
assert.ok(t.highAhead < c.liveBufferLatencyMaxLatency);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('télémétrie : une coupure (waiting → playing) est remontée avec sa durée', async () => {
|
test('télémétrie : une coupure (waiting → playing) est remontée avec sa durée', async () => {
|
||||||
@@ -155,13 +175,62 @@ const PROVIDER_OUTAGE_S = 18;
|
|||||||
|
|
||||||
test('live : la réserve visée couvre un silence du panneau de ~18 s', () => {
|
test('live : la réserve visée couvre un silence du panneau de ~18 s', () => {
|
||||||
const c = liveMpegtsConfig();
|
const c = liveMpegtsConfig();
|
||||||
// liveSync ramène le retard vers la cible : c'est la réserve en régime
|
const t = liveTuning();
|
||||||
// établi. Elle doit dépasser le silence mesuré.
|
// Le régulateur ramène l'avance vers la cible : c'est la réserve en
|
||||||
assert.ok(c.liveSyncTargetLatency > PROVIDER_OUTAGE_S,
|
// régime établi. Elle doit dépasser le silence mesuré — et le ralenti
|
||||||
`cible ${c.liveSyncTargetLatency}s < silence mesuré ${PROVIDER_OUTAGE_S}s`);
|
// doit se déclencher AVANT d'être passé sous ce seuil de sécurité.
|
||||||
|
assert.ok(t.targetAhead > PROVIDER_OUTAGE_S,
|
||||||
|
`cible ${t.targetAhead}s < silence mesuré ${PROVIDER_OUTAGE_S}s`);
|
||||||
|
assert.ok(t.lowAhead > PROVIDER_OUTAGE_S);
|
||||||
assert.ok(c.liveBufferLatencyMinRemain > PROVIDER_OUTAGE_S);
|
assert.ok(c.liveBufferLatencyMinRemain > PROVIDER_OUTAGE_S);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('régulateur : ralentit sous la réserve, accélère au-dessus, sans osciller', () => {
|
||||||
|
const listeners = {};
|
||||||
|
let ahead = 0;
|
||||||
|
const video = {
|
||||||
|
addEventListener(type, fn) { (listeners[type] ||= []).push(fn); },
|
||||||
|
removeEventListener() {},
|
||||||
|
canPlayType: () => '',
|
||||||
|
play: () => Promise.resolve(),
|
||||||
|
pause() {},
|
||||||
|
get buffered() { return { length: 1, start: () => 0, end: () => 100 + ahead }; },
|
||||||
|
seekable: { length: 0 },
|
||||||
|
currentTime: 100,
|
||||||
|
playbackRate: 1,
|
||||||
|
paused: false,
|
||||||
|
seeking: false,
|
||||||
|
};
|
||||||
|
const window = {
|
||||||
|
addEventListener() {}, removeEventListener() {},
|
||||||
|
console: { log() {} },
|
||||||
|
location: { origin: 'https://xf.test', href: 'https://xf.test/', search: '' },
|
||||||
|
parent: { postMessage() {} },
|
||||||
|
};
|
||||||
|
vm.runInContext(source, vm.createContext({
|
||||||
|
window, location: window.location,
|
||||||
|
document: { addEventListener() {}, removeEventListener() {} },
|
||||||
|
URLSearchParams, setInterval: () => 0, clearInterval() {}, setTimeout, Date,
|
||||||
|
}));
|
||||||
|
const t = window.XFPlayerLiveTuning;
|
||||||
|
const p = new window.XFPlayer({ video, url: '/api/live/1/turbo.ts', type: 'live' });
|
||||||
|
p.mpegts = {};
|
||||||
|
p._started = true;
|
||||||
|
|
||||||
|
ahead = 13; p._tuneRate(); // session réelle : 13 s
|
||||||
|
assert.equal(video.playbackRate, t.slowRate);
|
||||||
|
ahead = t.lowAhead + 2; p._tuneRate(); // remonte, pas encore la cible
|
||||||
|
assert.equal(video.playbackRate, t.slowRate, 'repasse à 1 trop tôt : oscille');
|
||||||
|
ahead = t.targetAhead; p._tuneRate();
|
||||||
|
assert.equal(video.playbackRate, 1);
|
||||||
|
ahead = t.highAhead + 5; p._tuneRate(); // rafale de rattrapage
|
||||||
|
assert.equal(video.playbackRate, t.fastRate);
|
||||||
|
ahead = t.highAhead - 2; p._tuneRate();
|
||||||
|
assert.equal(video.playbackRate, t.fastRate, 'freine trop tôt : oscille');
|
||||||
|
ahead = t.targetAhead + 2; p._tuneRate();
|
||||||
|
assert.equal(video.playbackRate, 1);
|
||||||
|
});
|
||||||
|
|
||||||
test('live : la rafale de rattrapage après un silence ne fait pas sauter le programme', () => {
|
test('live : la rafale de rattrapage après un silence ne fait pas sauter le programme', () => {
|
||||||
// Mesuré en prod : après un silence, le panneau renvoie d'un coup tout le
|
// Mesuré en prod : après un silence, le panneau renvoie d'un coup tout le
|
||||||
// retard ; l'avance a dépassé 45 s et le lecteur a sauté 2 fois de 20 s
|
// retard ; l'avance a dépassé 45 s et le lecteur a sauté 2 fois de 20 s
|
||||||
@@ -170,8 +239,8 @@ test('live : la rafale de rattrapage après un silence ne fait pas sauter le pro
|
|||||||
const c = liveMpegtsConfig();
|
const c = liveMpegtsConfig();
|
||||||
assert.ok(c.liveBufferLatencyMaxLatency >= 2 * observedPeakAhead - 10,
|
assert.ok(c.liveBufferLatencyMaxLatency >= 2 * observedPeakAhead - 10,
|
||||||
`saut dès ${c.liveBufferLatencyMaxLatency}s d'avance : la rafale de rattrapage le déclenche`);
|
`saut dès ${c.liveBufferLatencyMaxLatency}s d'avance : la rafale de rattrapage le déclenche`);
|
||||||
assert.ok(c.liveSyncMaxLatency < observedPeakAhead,
|
assert.ok(liveTuning().highAhead < observedPeakAhead,
|
||||||
'liveSync doit déjà accélérer avant ce niveau');
|
'le régulateur doit déjà accélérer avant ce niveau');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('live : la vidéo déjà vue est purgée plus tôt, l\'avance étant plus grande', () => {
|
test('live : la vidéo déjà vue est purgée plus tôt, l\'avance étant plus grande', () => {
|
||||||
|
|||||||
+59
-7
@@ -66,6 +66,24 @@
|
|||||||
// Au-delà, on relance quand même avec ce qu'on a.
|
// Au-delà, on relance quand même avec ce qu'on a.
|
||||||
var REBUFFER_MAX_WAIT_MS = 15000;
|
var REBUFFER_MAX_WAIT_MS = 15000;
|
||||||
|
|
||||||
|
// Régulateur de vitesse du direct MPEG-TS (voir _tuneRate).
|
||||||
|
//
|
||||||
|
// La réserve ne venait que de l'avance envoyée par le panneau au
|
||||||
|
// démarrage : 28 s une fois, 18 s une autre — et avec 13-20 s, un silence
|
||||||
|
// du panneau a fini en coupure de 5,5 s (télémétrie). Sous lowAhead, on
|
||||||
|
// lit à x0,97 (hauteur du son préservée) jusqu'à reconstituer targetAhead ;
|
||||||
|
// au-delà de highAhead (rafale de rattrapage du panneau), x1,1 jusqu'à
|
||||||
|
// settleHigh. Les seuils d'entrée et de sortie diffèrent : pas
|
||||||
|
// d'oscillation de vitesse.
|
||||||
|
var LIVE_TUNING = {
|
||||||
|
lowAhead: 20,
|
||||||
|
targetAhead: 25,
|
||||||
|
settleHigh: 28,
|
||||||
|
highAhead: 35,
|
||||||
|
slowRate: 0.97,
|
||||||
|
fastRate: 1.1
|
||||||
|
};
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// Chargement paresseux des libs (aucune requête inutile)
|
// Chargement paresseux des libs (aucune requête inutile)
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
@@ -516,16 +534,15 @@
|
|||||||
// Saut seulement au-delà de 90 s : après un silence, le panneau
|
// Saut seulement au-delà de 90 s : après un silence, le panneau
|
||||||
// renvoie d'un coup tout le retard. Avec un seuil à 45 s, l'avance
|
// renvoie d'un coup tout le retard. Avec un seuil à 45 s, l'avance
|
||||||
// le dépassait et le lecteur sautait 20 s de programme (2 fois en
|
// le dépassait et le lecteur sautait 20 s de programme (2 fois en
|
||||||
// 1 min, mesuré). liveSync résorbe désormais cet excédent en douceur.
|
// 1 min, mesuré). Le régulateur de vitesse (_tuneRate) résorbe
|
||||||
|
// désormais cet excédent en douceur.
|
||||||
liveBufferLatencyChasing: isLive,
|
liveBufferLatencyChasing: isLive,
|
||||||
liveBufferLatencyMaxLatency: 90,
|
liveBufferLatencyMaxLatency: 90,
|
||||||
liveBufferLatencyMinRemain: 25,
|
liveBufferLatencyMinRemain: 25,
|
||||||
// Au-delà de 35 s de retard, accélérer légèrement (x1,1) jusqu'à
|
// liveSync de mpegts.js désactivé : il ne sait qu'accélérer et remet
|
||||||
// revenir à 25 s : rattrapage invisible, sans saut ni coupure.
|
// la vitesse à 1 sous sa cible, ce qui annulerait le ralenti de
|
||||||
liveSync: isLive,
|
// _tuneRate qui reconstitue la réserve.
|
||||||
liveSyncMaxLatency: 35,
|
liveSync: false,
|
||||||
liveSyncTargetLatency: 25,
|
|
||||||
liveSyncPlaybackRate: 1.1,
|
|
||||||
// Purge du buffer déjà lu : sans elle, une longue séance finit par
|
// Purge du buffer déjà lu : sans elle, une longue séance finit par
|
||||||
// saturer le SourceBuffer (QuotaExceededError → erreur MPEG-TS).
|
// saturer le SourceBuffer (QuotaExceededError → erreur MPEG-TS).
|
||||||
// Avance jusqu'à 90 s : la vidéo déjà vue est purgée plus tôt pour
|
// Avance jusqu'à 90 s : la vidéo déjà vue est purgée plus tôt pour
|
||||||
@@ -713,6 +730,36 @@
|
|||||||
this._holdTimer = setInterval(function () { self._checkHold(); }, 250);
|
this._holdTimer = setInterval(function () { self._checkHold(); }, 250);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ajuste la vitesse du direct MPEG-TS selon l'avance (voir LIVE_TUNING).
|
||||||
|
* Appelé chaque seconde ; sans effet en pause, en attente de réserve,
|
||||||
|
* hors direct ou en HLS.
|
||||||
|
*/
|
||||||
|
XFPlayer.prototype._tuneRate = function () {
|
||||||
|
var v = this.video;
|
||||||
|
if (!this.mpegts || this.type !== 'live' || !this._started ||
|
||||||
|
this._holding || v.paused) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var ahead = this._bufferAhead();
|
||||||
|
var t = LIVE_TUNING;
|
||||||
|
var mode = this._rateMode || 'normal';
|
||||||
|
if (mode === 'slow' && ahead >= t.targetAhead) mode = 'normal';
|
||||||
|
else if (mode === 'fast' && ahead <= t.settleHigh) mode = 'normal';
|
||||||
|
else if (mode === 'normal' && ahead < t.lowAhead) mode = 'slow';
|
||||||
|
else if (mode === 'normal' && ahead > t.highAhead) mode = 'fast';
|
||||||
|
// Un saut de rattrapage peut faire passer directement d'un extrême à
|
||||||
|
// l'autre.
|
||||||
|
if (mode === 'slow' && ahead > t.highAhead) mode = 'fast';
|
||||||
|
if (mode === 'fast' && ahead < t.lowAhead) mode = 'slow';
|
||||||
|
this._rateMode = mode;
|
||||||
|
|
||||||
|
var rate = mode === 'slow' ? t.slowRate : (mode === 'fast' ? t.fastRate : 1);
|
||||||
|
if (v.playbackRate !== rate) {
|
||||||
|
try { v.playbackRate = rate; } catch (e) {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/** Relance la lecture dès que la réserve est reconstituée. */
|
/** Relance la lecture dès que la réserve est reconstituée. */
|
||||||
XFPlayer.prototype._checkHold = function () {
|
XFPlayer.prototype._checkHold = function () {
|
||||||
if (!this._holding) return;
|
if (!this._holding) return;
|
||||||
@@ -796,6 +843,9 @@
|
|||||||
self._report('seek', { from: self._lastPos, to: v.currentTime });
|
self._report('seek', { from: self._lastPos, to: v.currentTime });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Régulateur de vitesse du direct (voir LIVE_TUNING).
|
||||||
|
this._rateTimer = setInterval(function () { self._tuneRate(); }, 1000);
|
||||||
|
|
||||||
// Bilan toutes les 60 s : nombre et durée cumulée des coupures.
|
// Bilan toutes les 60 s : nombre et durée cumulée des coupures.
|
||||||
this._beatTimer = setInterval(function () {
|
this._beatTimer = setInterval(function () {
|
||||||
if (!self._started) return;
|
if (!self._started) return;
|
||||||
@@ -982,6 +1032,7 @@
|
|||||||
clearInterval(this._posTimer);
|
clearInterval(this._posTimer);
|
||||||
clearInterval(this._beatTimer);
|
clearInterval(this._beatTimer);
|
||||||
clearInterval(this._holdTimer);
|
clearInterval(this._holdTimer);
|
||||||
|
clearInterval(this._rateTimer);
|
||||||
this._listeners.forEach(function (l) {
|
this._listeners.forEach(function (l) {
|
||||||
try { l.target.removeEventListener(l.type, l.handler); } catch (e) {}
|
try { l.target.removeEventListener(l.type, l.handler); } catch (e) {}
|
||||||
});
|
});
|
||||||
@@ -998,4 +1049,5 @@
|
|||||||
|
|
||||||
global.XFPlayer = XFPlayer;
|
global.XFPlayer = XFPlayer;
|
||||||
global.XFPlayerProfiles = PROFILES;
|
global.XFPlayerProfiles = PROFILES;
|
||||||
|
global.XFPlayerLiveTuning = LIVE_TUNING;
|
||||||
})(window);
|
})(window);
|
||||||
Reference in new issue
Block a user