From 424019d01ac985251ede1312de11063b4aedb53e Mon Sep 17 00:00:00 2001 From: Michael Date: Sun, 8 Mar 2026 23:38:57 +0100 Subject: [PATCH] =?UTF-8?q?fix(scheduler):=20wrap=20=5FstartRecording=20in?= =?UTF-8?q?=20single=20try/catch=20to=20prevent=20server=20crash,=20fix=20?= =?UTF-8?q?localhost=20port=208080=E2=86=928089?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/services/recording_scheduler.dart | 141 ++++++++++++-------------- 1 file changed, 66 insertions(+), 75 deletions(-) diff --git a/bin/services/recording_scheduler.dart b/bin/services/recording_scheduler.dart index 7d04f9e..762c56b 100644 --- a/bin/services/recording_scheduler.dart +++ b/bin/services/recording_scheduler.dart @@ -91,6 +91,8 @@ class RecordingScheduler { _currentRecording = recording; _db.updateRecordingStatus(recording.id, 'recording'); + // Un seul bloc try/catch englobant TOUT pour éviter les Unhandled exceptions + // qui tueraient le serveur entier try { // Préparation du dossier d'enregistrement final recordingsDir = Directory('/app/recordings'); @@ -98,98 +100,87 @@ class RecordingScheduler { await recordingsDir.create(recursive: true); } - // Nettoyer l'espace disque si nécessaire (TODO : Implémenter logique de roulement ici) + // Nettoyer l'espace disque si nécessaire await _checkDiskSpaceAndRotate(recordingsDir); // Génération d'un nom de fichier unique et sûr final safeTitle = recording.title.replaceAll(RegExp(r'[^a-zA-Z0-9_\-]'), '_'); - final dateStr = recording.startTime.toIso8601String().replaceAll(':', '').split('.')[0]; + final dateStr = recording.startTime.toUtc().toIso8601String().replaceAll(':', '').split('.')[0]; final fileName = '${safeTitle}_$dateStr.mp4'; final filePath = p.join(recordingsDir.path, fileName); + final logFilePath = filePath.replaceAll('.mp4', '.log'); - // FFmpeg arguments pour enregistrement direct (copie sans transcodage = bien plus rapide) - // On résout l'URL relative en URL absolue pour que FFmpeg puisse y accéder depuis Docker + // Résoudre l'URL relative en URL absolue pour FFmpeg + // Le serveur Xtremflow tourne sur le port 8089 en interne Docker String streamUrl = recording.streamUrl; if (streamUrl.startsWith('/')) { - // URL relative → URL absolue (le serveur Xtremflow tourne sur le port 8080 en interne) - streamUrl = 'http://localhost:8080$streamUrl'; + streamUrl = 'http://localhost:8089$streamUrl'; } - + final args = [ - '-y', // Force overwrite - '-i', streamUrl, // Input stream (URL absolue) - '-c', 'copy', // Copie directe sans transcodage (beaucoup plus rapide et fiable) - // Arrêter automatiquement après la durée - '-t', '${recording.endTime.difference(DateTime.now()).inSeconds}', + '-y', + '-i', streamUrl, + '-c', 'copy', + '-t', '${recording.endTime.difference(DateTime.now()).inSeconds}', filePath ]; - // Création immédiate du fichier de log pour capturer TOUTES les erreurs - final logFilePath = filePath.replaceAll('.mp4', '.log'); - final logFile = File(logFilePath); - final logSink = logFile.openWrite(); - - logSink.writeln('[${DateTime.now()}] Démarrage de l\'enregistrement : ${recording.title}'); - logSink.writeln('URL source: ${recording.streamUrl}'); - logSink.writeln('Fichier destination: $filePath'); - + // Créer le fichier de log de façon sécurisée (openWrite peut lancer une exception) + IOSink? logSink; try { - // En Docker, ffmpeg a été installé dans /usr/local/bin/ffmpeg - // On teste d'abord s'il existe à cet endroit, sinon on utilise le nom court (pour dev local) - String ffmpegPath = 'ffmpeg'; - if (Platform.isLinux && await File('/usr/local/bin/ffmpeg').exists()) { - ffmpegPath = '/usr/local/bin/ffmpeg'; - } - - logSink.writeln('Commande: $ffmpegPath ${args.join(' ')}\n'); - print('[RecordingScheduler] Exécution: $ffmpegPath ${args.join(' ')}'); - - _ffmpegProcess = await Process.start(ffmpegPath, args); - - // Rediriger la sortie de ffmpeg vers ce fichier - _ffmpegProcess!.stdout.listen((event) { - logSink.add(event); - }); - _ffmpegProcess!.stderr.listen((event) { - logSink.add(event); // FFmpeg écrit sa progression et ses erreurs ici - }); - - // Enregistrer le chemin du fichier dans la BDD (et donc marquer le log comme disponible) + logSink = File(logFilePath).openWrite(); + logSink.writeln('[${DateTime.now()}] Démarrage: ${recording.title}'); + logSink.writeln('URL: $streamUrl'); + logSink.writeln('Destination: $filePath'); + } catch (logError) { + // Si on ne peut pas créer le log, on continue quand même (l'enregistrement reste prioritaire) + print('[RecordingScheduler] AVERTISSEMENT: Impossible de créer le fichier log ($logFilePath): $logError'); + // On note la filePath dans la DB quand même pour pouvoir retourner le statut _db.updateRecordingStatus(recording.id, 'recording', filePath: filePath); - - // Écouter de manière asynchrone la fin du processus FFmpeg - _ffmpegProcess!.exitCode.then((exitCode) async { - if (_currentRecording?.id == recording.id) { - logSink.writeln('\n[${DateTime.now()}] Processus FFmpeg terminé avec le code $exitCode'); - await logSink.close(); - - if (exitCode == 0 || exitCode == 255) { // 255 est souvent renvoyé lors d'un arrêt forcé - print('[RecordingScheduler] Enregistrement terminé avec succès (${recording.title})'); - _db.updateRecordingStatus(recording.id, 'completed'); - } else { - print('[RecordingScheduler] Erreur FFmpeg (code: $exitCode) pour l\'enregistrement ${recording.title}'); - _db.updateRecordingStatus(recording.id, 'failed', errorReason: 'Erreur FFmpeg code $exitCode. Voir logs.'); - } - _currentRecording = null; - _ffmpegProcess = null; - } else { - await logSink.close(); - } - }); - - } catch (e) { - logSink.writeln('\n[${DateTime.now()}] ERREUR CRITIQUE AU LANCEMENT: $e'); - await logSink.close(); - - print('[RecordingScheduler] Impossible de démarrer FFmpeg: $e'); - _db.updateRecordingStatus(recording.id, 'failed', errorReason: 'Impossible de lancer FFmpeg: $e', filePath: filePath); - _currentRecording = null; - _ffmpegProcess = null; } - - } catch (e) { - print('[RecordingScheduler] Erreur inattendue avant lancement: $e'); - _db.updateRecordingStatus(recording.id, 'failed', errorReason: 'Erreur inattendue: $e'); + + // Trouver ffmpeg + String ffmpegPath = 'ffmpeg'; + if (Platform.isLinux && await File('/usr/local/bin/ffmpeg').exists()) { + ffmpegPath = '/usr/local/bin/ffmpeg'; + } + + logSink?.writeln('Commande: $ffmpegPath ${args.join(' ')}\n'); + print('[RecordingScheduler] Exécution: $ffmpegPath ${args.join(' ')}'); + + _ffmpegProcess = await Process.start(ffmpegPath, args); + + // Rediriger stdout/stderr dans le log + _ffmpegProcess!.stdout.listen((event) => logSink?.add(event)); + _ffmpegProcess!.stderr.listen((event) => logSink?.add(event)); + + // Enregistrer le chemin du fichier dans la BDD + _db.updateRecordingStatus(recording.id, 'recording', filePath: filePath); + + // Écouter la fin du processus FFmpeg de manière asynchrone + _ffmpegProcess!.exitCode.then((exitCode) async { + if (_currentRecording?.id == recording.id) { + logSink?.writeln('\n[${DateTime.now()}] FFmpeg terminé avec le code $exitCode'); + await logSink?.close(); + + if (exitCode == 0 || exitCode == 255) { + print('[RecordingScheduler] Enregistrement terminé: ${recording.title}'); + _db.updateRecordingStatus(recording.id, 'completed'); + } else { + print('[RecordingScheduler] Erreur FFmpeg (code: $exitCode) pour ${recording.title}'); + _db.updateRecordingStatus(recording.id, 'failed', errorReason: 'Erreur FFmpeg code $exitCode. Voir logs.'); + } + _currentRecording = null; + _ffmpegProcess = null; + } else { + await logSink?.close(); + } + }); + + } catch (e, st) { + // Attraper TOUTES les exceptions pour éviter de crasher le serveur + print('[RecordingScheduler] ERREUR dans _startRecording: $e\n$st'); + _db.updateRecordingStatus(recording.id, 'failed', errorReason: 'Erreur au lancement: $e'); _currentRecording = null; _ffmpegProcess = null; }