mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-11 17:30:00 +02:00
Lecture TV : fin des coupures en rafale sur le direct
Les panneaux Xtream livrent le flux par rafales (~6 s de contenu puis plusieurs secondes de silence). Le rattrapage mpegts.js réglé à 5 s max / 1 s de marge sautait en avant à chaque rafale puis tombait à sec : mesuré en prod, 12 sauts et 14 coupures en 30 s sur une chaîne FHD. Marge portée à 12 s, saut seulement au-delà de 30 s de retard, retard résorbé par liveSync (x1,1) et purge du buffer déjà lu. Rejoué en prod avec cette config : 0 coupure en continu. Ajoute des tests Node du moteur web, lancés par la CI. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
327873d254
commit
a9b43ff581
3 files changed
+135
-7
No files matched your search
@@ -31,6 +31,9 @@ jobs:
|
||||
# fatal; errors and warnings still fail the build.
|
||||
- run: flutter analyze --no-fatal-infos
|
||||
- run: flutter test
|
||||
# Moteur de lecture web (web/xf-player-core.js) : Node est préinstallé
|
||||
# sur les runners ubuntu, aucune dépendance npm.
|
||||
- run: node --test "test/web/*_test.mjs"
|
||||
- run: flutter build web --release
|
||||
|
||||
backend:
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
// Tests du moteur de lecture web (web/xf-player-core.js).
|
||||
//
|
||||
// Lancement : node --test "test/web/*_test.mjs"
|
||||
//
|
||||
// Le moteur est un script navigateur (IIFE sur `window`) : on l'exécute dans
|
||||
// un contexte vm avec un DOM minimal et un faux mpegts.js qui capture la
|
||||
// configuration reçue.
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import vm from 'node:vm';
|
||||
|
||||
const source = readFileSync(
|
||||
new URL('../../web/xf-player-core.js', import.meta.url),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
function fakeEventTarget() {
|
||||
return { addEventListener() {}, removeEventListener() {} };
|
||||
}
|
||||
|
||||
/** Charge le moteur et renvoie la config passée à mpegts.createPlayer. */
|
||||
function liveMpegtsConfig({ profile } = {}) {
|
||||
const created = [];
|
||||
const video = {
|
||||
...fakeEventTarget(),
|
||||
canPlayType: () => '',
|
||||
play: () => Promise.resolve(),
|
||||
buffered: { length: 0 },
|
||||
seekable: { length: 0 },
|
||||
};
|
||||
const window = {
|
||||
...fakeEventTarget(),
|
||||
console: { log() {} },
|
||||
location: { origin: 'https://xf.test', href: 'https://xf.test/', search: '' },
|
||||
parent: { postMessage() {} },
|
||||
mpegts: {
|
||||
isSupported: () => true,
|
||||
Events: { MEDIA_INFO: 'media_info', ERROR: 'error' },
|
||||
createPlayer(mediaDataSource, config) {
|
||||
created.push({ mediaDataSource, config });
|
||||
return { on() {}, attachMediaElement() {}, load() {} };
|
||||
},
|
||||
},
|
||||
};
|
||||
const context = vm.createContext({
|
||||
window,
|
||||
location: window.location,
|
||||
document: fakeEventTarget(),
|
||||
URLSearchParams,
|
||||
setInterval: () => 0,
|
||||
clearInterval() {},
|
||||
setTimeout,
|
||||
});
|
||||
vm.runInContext(source, context);
|
||||
|
||||
const XFPlayer = window.XFPlayer;
|
||||
const player = new XFPlayer({
|
||||
video,
|
||||
url: '/api/live/1/turbo.ts',
|
||||
type: 'live',
|
||||
});
|
||||
if (profile) player.profile = window.XFPlayerProfiles[profile];
|
||||
player._createMpegts(true);
|
||||
assert.equal(created.length, 1);
|
||||
return created[0].config;
|
||||
}
|
||||
|
||||
// Les panneaux Xtream relaient souvent une source HLS : le flux arrive par
|
||||
// rafales d'environ 6 s séparées de silences complets (mesuré en prod).
|
||||
const UPSTREAM_BURST_GAP_S = 7;
|
||||
|
||||
for (const profile of ['fast', 'balanced', 'safe']) {
|
||||
test(`live (${profile}) : la marge gardée après rattrapage couvre un silence amont`, () => {
|
||||
const c = liveMpegtsConfig({ profile });
|
||||
if (!c.liveBufferLatencyChasing) return; // pas de saut = rien à vérifier
|
||||
assert.ok(
|
||||
c.liveBufferLatencyMinRemain >= UPSTREAM_BURST_GAP_S,
|
||||
`MinRemain ${c.liveBufferLatencyMinRemain}s < silence amont ${UPSTREAM_BURST_GAP_S}s : ` +
|
||||
'chaque rafale déclenche un saut puis une coupure',
|
||||
);
|
||||
});
|
||||
|
||||
test(`live (${profile}) : une rafale normale ne déclenche pas de saut`, () => {
|
||||
const c = liveMpegtsConfig({ profile });
|
||||
if (!c.liveBufferLatencyChasing) return;
|
||||
// Après une rafale, le buffer vaut la marge + une rafale entière.
|
||||
assert.ok(
|
||||
c.liveBufferLatencyMaxLatency >=
|
||||
c.liveBufferLatencyMinRemain + 2 * UPSTREAM_BURST_GAP_S,
|
||||
'seuil de rattrapage trop proche de la marge : sauts à chaque rafale',
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
test('live : le retard est résorbé en douceur (liveSync) plutôt que par sauts', () => {
|
||||
const c = liveMpegtsConfig();
|
||||
assert.equal(c.liveSync, true);
|
||||
assert.ok(c.liveSyncPlaybackRate > 1 && c.liveSyncPlaybackRate <= 1.2);
|
||||
assert.ok(c.liveSyncTargetLatency < c.liveSyncMaxLatency);
|
||||
assert.ok(c.liveSyncMaxLatency <= c.liveBufferLatencyMaxLatency);
|
||||
});
|
||||
|
||||
test('live : le buffer déjà lu est purgé (sinon QuotaExceeded en séance longue)', () => {
|
||||
const c = liveMpegtsConfig();
|
||||
assert.equal(c.autoCleanupSourceBuffer, true);
|
||||
assert.ok(
|
||||
c.autoCleanupMinBackwardDuration < c.autoCleanupMaxBackwardDuration,
|
||||
);
|
||||
});
|
||||
+22
-7
@@ -419,13 +419,28 @@
|
||||
// 64 Ko en profil « fast » contre 512 Ko auparavant.
|
||||
stashInitialSize: this.profile.mpegtsStash,
|
||||
maxStashSize: 30 * 1024 * 1024,
|
||||
// Rattrapage du direct : sans lui, chaque micro-coupure fait dériver
|
||||
// la lecture derrière le direct (latence qui s'accumule). Activé
|
||||
// uniquement en profil « fast » — les profils élargis privilégient
|
||||
// la stabilité du buffer.
|
||||
liveBufferLatencyChasing: isLive && this.profile.name === 'fast',
|
||||
liveBufferLatencyMaxLatency: 5,
|
||||
liveBufferLatencyMinRemain: 1,
|
||||
// POURQUOI ces seuils : les panneaux Xtream relaient le plus souvent
|
||||
// une source HLS et livrent le flux par RAFALES (~6 s de contenu d'un
|
||||
// coup, puis plusieurs secondes de silence total — mesuré en prod).
|
||||
// L'ancien rattrapage (5 s max, 1 s de marge) sautait en avant à
|
||||
// chaque rafale puis tombait à sec pendant le silence suivant :
|
||||
// coupure toutes les 3-4 s et contenu sauté (12 sauts et 14 coupures
|
||||
// en 30 s sur une chaîne FHD). La marge de 12 s couvre deux silences ;
|
||||
// le saut ne sert plus qu'à borner un retard réellement accumulé.
|
||||
liveBufferLatencyChasing: isLive,
|
||||
liveBufferLatencyMaxLatency: 30,
|
||||
liveBufferLatencyMinRemain: 12,
|
||||
// Au-delà de 20 s de retard, accélérer légèrement (x1,1) jusqu'à
|
||||
// revenir à 12 s : rattrapage invisible, sans saut ni coupure.
|
||||
liveSync: isLive,
|
||||
liveSyncMaxLatency: 20,
|
||||
liveSyncTargetLatency: 12,
|
||||
liveSyncPlaybackRate: 1.1,
|
||||
// Purge du buffer déjà lu : sans elle, une longue séance finit par
|
||||
// saturer le SourceBuffer (QuotaExceededError → erreur MPEG-TS).
|
||||
autoCleanupSourceBuffer: true,
|
||||
autoCleanupMaxBackwardDuration: 60,
|
||||
autoCleanupMinBackwardDuration: 30,
|
||||
lazyLoad: false
|
||||
}
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user