Lecture vidéo : son garanti sur toutes les chaînes, zapping et démarrages accélérés

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
This commit is contained in:
Claude committed 2026-08-27 21:17:27 +00:00
1 parent 60d3cc52d9
commit 85a8d16542
9 files changed
+136 -27

No files matched your search

+99 -4
View File
@@ -94,7 +94,11 @@ List<String> _liveVideoArgs(String quality, bool gpu) {
'-g', '50',
]
: [
'-c:v', 'libx264', '-preset', 'medium', '-tune', 'zerolatency',
// veryfast au lieu de medium : en live, l'encodeur doit tenir
// le temps réel ET produire le premier segment vite ; medium
// ajoutait plusieurs secondes au zap sans gain visible à ce
// débit.
'-c:v', 'libx264', '-preset', 'veryfast', '-tune', 'zerolatency',
'-profile:v', 'high', '-level', '4.0',
'-b:v', '6000k', '-maxrate', '8000k', '-bufsize', '12000k',
'-pix_fmt', 'yuv420p', '-g', '50',
@@ -318,6 +322,11 @@ Handler createLiveStreamHandler(
// Abort reads stuck for 30s so a stalled upstream triggers the
// reconnect logic instead of wedging the process forever.
'-rw_timeout', '30000000',
// Démarrage rapide : sans borne, FFmpeg peut passer plusieurs
// secondes à sonder le flux avant d'écrire le premier segment.
'-fflags', 'nobuffer',
'-probesize', '1000000',
'-analyzeduration', '1000000',
'-i', targetUrl,
..._liveVideoArgs(quality, useNvidiaGpu),
..._audioArgs(withFilters: false),
@@ -364,6 +373,85 @@ Handler createLiveStreamHandler(
return Response.found('/api/live/$streamId/$quality/playlist.m3u8');
});
// Route: /api/live/{streamId}/turbo.ts — flux MPEG-TS continu pour le
// player web (mpegts.js) : vidéo copiée telle quelle, audio TOUJOURS
// réencodé en AAC.
//
// POURQUOI : mpegts.js ne démuxe que l'AAC et le MP3. Les chaînes qui
// diffusent en AC-3/E-AC-3 (0x81/0x87) ou en MPEG-1 Layer II passaient par
// le proxy brut → image sans aucun son. Le réencodage audio seul coûte
// quelques % de CPU (pas de transcodage vidéo) et garde le zapping
// instantané : un seul flux continu, pas de segmentation HLS à amorcer.
//
// Latence : -fflags nobuffer + probesize réduit → première image en
// ~0,5-1,5 s au lieu des 2-6 s d'analyse par défaut de FFmpeg.
router.get('/<streamId>/turbo.ts', (Request request, String streamId) async {
if (!_isValidStreamId(streamId)) {
return Response.badRequest(body: 'Invalid stream ID');
}
final playlist = await getPlaylist(request);
if (playlist == null) return Response.forbidden('No playlist');
final targetUrl =
'${playlist.dns}/live/${playlist.username}/${playlist.password}/$streamId.ts';
print(
'[Live Turbo] $streamId: ${LogRedactor.redactUrl(targetUrl)}',
);
final process = await Process.start(_getFFmpegPath(), [
'-hide_banner', '-loglevel', 'warning',
'-headers', 'User-Agent: VLC/3.0.18 LibVLC/3.0.18\r\n',
'-reconnect', '1', '-reconnect_streamed', '1',
'-reconnect_at_eof', '1',
'-reconnect_delay_max', '10',
'-rw_timeout', '30000000',
// Démarrage rapide : ne pas bufferiser l'analyse, sonde réduite.
'-fflags', 'nobuffer',
'-flags', 'low_delay',
'-probesize', '1000000',
'-analyzeduration', '1000000',
'-i', targetUrl,
'-c:v', 'copy',
'-c:a', 'aac', '-b:a', '160k', '-ac', '2', '-ar', '48000',
'-af', 'aresample=async=1',
// Pas de délai de mux : les paquets partent dès qu'ils existent.
'-muxdelay', '0', '-muxpreload', '0',
'-f', 'mpegts', 'pipe:1',
]);
// Journaliser les erreurs FFmpeg (redactées) sans bloquer le flux.
process.stderr.transform(const SystemEncoding().decoder).listen((line) {
final trimmed = line.trim();
if (trimmed.isNotEmpty) {
print('[Live Turbo] $streamId ffmpeg: '
'${LogRedactor.redactUrl(trimmed)}');
}
});
// Relayer stdout vers le client ; tuer FFmpeg dès que le client zappe
// ou ferme l'onglet (sinon les processus s'accumulent à chaque zap).
final controller = StreamController<List<int>>();
final subscription = process.stdout.listen(
controller.add,
onError: controller.addError,
onDone: controller.close,
);
controller.onCancel = () {
subscription.cancel();
process.kill(ProcessSignal.sigterm);
};
return Response(
200,
body: controller.stream,
headers: {
'Content-Type': 'video/mp2t',
'Cache-Control': 'no-store',
'Connection': 'keep-alive',
},
);
});
// Route: /api/live/{streamId}.ts (Direct Proxy for recordings/raw playback)
router.get('/<streamId>.ts', (Request request, String streamId) async {
final playlist = await getPlaylist(request);
@@ -464,8 +552,11 @@ Handler createVodStreamHandler(
'-reconnect_delay_max', '10',
'-rw_timeout', '30000000',
'-timeout', '30000000',
'-analyzeduration', '5000000',
'-probesize', '10000000',
// Sonde réduite (10 Mo → 5 Mo) : sur un upstream lent, télécharger
// 10 Mo avant la première image ajoutait plusieurs secondes au
// démarrage de chaque film.
'-analyzeduration', '2000000',
'-probesize', '5000000',
'-i', targetUrl,
..._vodVideoArgs(quality, useNvidiaGpu),
..._audioArgs(withFilters: quality != 'source'),
@@ -586,7 +677,11 @@ Handler createRecordingStreamHandler(
'-bufsize', '6000k',
'-g', '48', '-bf', '2', '-pix_fmt', 'yuv420p',
] else ...[
'-c:v', 'libx264', '-preset', 'medium', '-crf', '18',
// veryfast/crf 20 au lieu de medium/crf 18 : à ce niveau le rendu
// est visuellement identique, mais medium ne tenait pas le temps
// réel en 1080p sur un CPU modeste → lecture d'enregistrement qui
// démarre lentement puis bufferise.
'-c:v', 'libx264', '-preset', 'veryfast', '-crf', '20',
'-maxrate', '12000k', '-bufsize', '24000k', '-pix_fmt', 'yuv420p',
'-g', '48', '-threads', '0',
],