From 2151f2820caeaced35dc41271429bd6726087461 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 10 Sep 2026 17:49:24 +0000 Subject: [PATCH] =?UTF-8?q?Lecture=20d'un=20enregistrement=20:=20repli=20s?= =?UTF-8?q?ur=20le=20r=C3=A9-encodage=20si=20la=20copie=20=C3=A9choue?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Certains conteneurs ou horodatages ne se prêtent pas à `-c:v copy`. Plutôt qu'un écran d'erreur définitif là où le ré-encodage fonctionnait jusqu'ici, la session est relancée une fois sans copie quand la première tentative ne produit pas de playlist. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01T1A6WRnoktWcT7ZG4b2rbC --- bin/api/streaming_handler.dart | 79 ++++++++++++++++++++++++---------- 1 file changed, 56 insertions(+), 23 deletions(-) diff --git a/bin/api/streaming_handler.dart b/bin/api/streaming_handler.dart index dbe9e8b..b08c4d0 100644 --- a/bin/api/streaming_handler.dart +++ b/bin/api/streaming_handler.dart @@ -681,21 +681,11 @@ Handler createRecordingStreamHandler( // (HEVC, MPEG-2…) justifient encore un ré-encodage. final videoCodec = await MediaProbe.videoCodec(targetUrl); final canCopyVideo = videoCodec == 'h264'; - final useNvidiaGpu = - !canCopyVideo && (isGpuEnabled?.call() ?? _isNvidiaGpuEnabled()); - if (!sessionManager.contains(sessionId)) { - print( - '[Recording] $sessionId : ${canCopyVideo ? 'copie vidéo' : 'ré-encodage'}' - ' (codec source : ${videoCodec ?? 'inconnu'})', - ); - } - - final session = await sessionManager.getOrStart( - id: sessionId, - isLive: false, - ffmpegPath: _getFFmpegPath(), - argsBuilder: (dir) => [ + List buildArgs({required bool copyVideo}) { + final useNvidiaGpu = + !copyVideo && (isGpuEnabled?.call() ?? _isNvidiaGpuEnabled()); + return [ '-hide_banner', '-loglevel', 'warning', if (useNvidiaGpu) ...['-hwaccel', 'cuda'], // -ss AVANT -i : recherche rapide par index, sans décoder ce qui @@ -703,7 +693,7 @@ Handler createRecordingStreamHandler( // En copie, la position atteinte est l'image-clé qui précède. if (start > 0) ...['-ss', '$start'], '-i', targetUrl, - if (canCopyVideo) ...[ + if (copyVideo) ...[ '-c:v', 'copy', ] else if (useNvidiaGpu) ...[ '-c:v', 'h264_nvenc', '-preset', 'p4', '-tune', 'hq', @@ -730,19 +720,62 @@ Handler createRecordingStreamHandler( '-hls_flags', 'independent_segments', '-hls_segment_type', 'mpegts', '-hls_segment_filename', 'segment_%03d.ts', '-start_number', '0', 'playlist.m3u8', - ], - ); + ]; + } - // Trois segments d'avance (~12 s) avant de rendre la main : c'est la - // marge qui manquait au démarrage. En copie vidéo elle est produite en - // une fraction de seconde, elle ne coûte donc que sur un ré-encodage. - final result = await sessionManager.waitForPlaylist(session, minSegments: 3); - if (!result.ready) { + /// Démarre (ou récupère) la session et attend qu'elle ait de l'avance. + /// + /// Trois segments avant de rendre la main : c'est la marge qui manquait + /// au démarrage, la playlist étant servie dès le premier segment. En + /// copie vidéo elle est produite en une fraction de seconde, elle ne + /// coûte donc que sur un ré-encodage. + Future<({FfmpegSession session, bool ready, String? error})> run( + bool copyVideo, + ) async { + if (!sessionManager.contains(sessionId)) { + print( + '[Recording] $sessionId : ' + '${copyVideo ? 'copie vidéo' : 'ré-encodage'} ' + '(codec source : ${videoCodec ?? 'inconnu'})', + ); + } + final session = await sessionManager.getOrStart( + id: sessionId, + isLive: false, + ffmpegPath: _getFFmpegPath(), + argsBuilder: (dir) => buildArgs(copyVideo: copyVideo), + ); + final outcome = + await sessionManager.waitForPlaylist(session, minSegments: 3); + return ( + session: session, + ready: outcome.ready, + error: outcome.error, + ); + } + + var attempt = await run(canCopyVideo); + + // La copie a échoué : certains conteneurs ou horodatages ne s'y prêtent + // pas. Plutôt qu'un écran d'erreur, on retombe sur le ré-encodage, qui + // était le seul chemin jusqu'ici. + if (!attempt.ready && canCopyVideo) { + print( + '[Recording] $sessionId : copie vidéo refusée (${attempt.error}) ' + '— repli sur le ré-encodage', + ); + sessionManager.killSession(sessionId); + attempt = await run(false); + } + + if (!attempt.ready) { sessionManager.killSession(sessionId); return Response(502, - body: 'Recording transcoder failed: ${result.error}'); + body: 'Recording transcoder failed: ${attempt.error}'); } + final session = attempt.session; + session.touch(); // Ménage des offsets abandonnés (aller-retours dans la barre de // progression), une fois la nouvelle session confirmée démarrée.