mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-11 17:30:00 +02:00
fix(scheduler): wrap _startRecording in single try/catch to prevent server crash, fix localhost port 8080→8089
This commit is contained in:
1 parent
93cecfc6d3
commit
424019d01a
1 file changed
+63
-72
@@ -91,6 +91,8 @@ class RecordingScheduler {
|
|||||||
_currentRecording = recording;
|
_currentRecording = recording;
|
||||||
_db.updateRecordingStatus(recording.id, 'recording');
|
_db.updateRecordingStatus(recording.id, 'recording');
|
||||||
|
|
||||||
|
// Un seul bloc try/catch englobant TOUT pour éviter les Unhandled exceptions
|
||||||
|
// qui tueraient le serveur entier
|
||||||
try {
|
try {
|
||||||
// Préparation du dossier d'enregistrement
|
// Préparation du dossier d'enregistrement
|
||||||
final recordingsDir = Directory('/app/recordings');
|
final recordingsDir = Directory('/app/recordings');
|
||||||
@@ -98,98 +100,87 @@ class RecordingScheduler {
|
|||||||
await recordingsDir.create(recursive: true);
|
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);
|
await _checkDiskSpaceAndRotate(recordingsDir);
|
||||||
|
|
||||||
// Génération d'un nom de fichier unique et sûr
|
// Génération d'un nom de fichier unique et sûr
|
||||||
final safeTitle = recording.title.replaceAll(RegExp(r'[^a-zA-Z0-9_\-]'), '_');
|
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 fileName = '${safeTitle}_$dateStr.mp4';
|
||||||
final filePath = p.join(recordingsDir.path, fileName);
|
final filePath = p.join(recordingsDir.path, fileName);
|
||||||
|
final logFilePath = filePath.replaceAll('.mp4', '.log');
|
||||||
|
|
||||||
// FFmpeg arguments pour enregistrement direct (copie sans transcodage = bien plus rapide)
|
// Résoudre l'URL relative en URL absolue pour FFmpeg
|
||||||
// On résout l'URL relative en URL absolue pour que FFmpeg puisse y accéder depuis Docker
|
// Le serveur Xtremflow tourne sur le port 8089 en interne Docker
|
||||||
String streamUrl = recording.streamUrl;
|
String streamUrl = recording.streamUrl;
|
||||||
if (streamUrl.startsWith('/')) {
|
if (streamUrl.startsWith('/')) {
|
||||||
// URL relative → URL absolue (le serveur Xtremflow tourne sur le port 8080 en interne)
|
streamUrl = 'http://localhost:8089$streamUrl';
|
||||||
streamUrl = 'http://localhost:8080$streamUrl';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final args = [
|
final args = [
|
||||||
'-y', // Force overwrite
|
'-y',
|
||||||
'-i', streamUrl, // Input stream (URL absolue)
|
'-i', streamUrl,
|
||||||
'-c', 'copy', // Copie directe sans transcodage (beaucoup plus rapide et fiable)
|
'-c', 'copy',
|
||||||
// Arrêter automatiquement après la durée
|
|
||||||
'-t', '${recording.endTime.difference(DateTime.now()).inSeconds}',
|
'-t', '${recording.endTime.difference(DateTime.now()).inSeconds}',
|
||||||
filePath
|
filePath
|
||||||
];
|
];
|
||||||
|
|
||||||
// Création immédiate du fichier de log pour capturer TOUTES les erreurs
|
// Créer le fichier de log de façon sécurisée (openWrite peut lancer une exception)
|
||||||
final logFilePath = filePath.replaceAll('.mp4', '.log');
|
IOSink? logSink;
|
||||||
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');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// En Docker, ffmpeg a été installé dans /usr/local/bin/ffmpeg
|
logSink = File(logFilePath).openWrite();
|
||||||
// On teste d'abord s'il existe à cet endroit, sinon on utilise le nom court (pour dev local)
|
logSink.writeln('[${DateTime.now()}] Démarrage: ${recording.title}');
|
||||||
String ffmpegPath = 'ffmpeg';
|
logSink.writeln('URL: $streamUrl');
|
||||||
if (Platform.isLinux && await File('/usr/local/bin/ffmpeg').exists()) {
|
logSink.writeln('Destination: $filePath');
|
||||||
ffmpegPath = '/usr/local/bin/ffmpeg';
|
} 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');
|
||||||
logSink.writeln('Commande: $ffmpegPath ${args.join(' ')}\n');
|
// On note la filePath dans la DB quand même pour pouvoir retourner le statut
|
||||||
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)
|
|
||||||
_db.updateRecordingStatus(recording.id, 'recording', filePath: filePath);
|
_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) {
|
// Trouver ffmpeg
|
||||||
print('[RecordingScheduler] Erreur inattendue avant lancement: $e');
|
String ffmpegPath = 'ffmpeg';
|
||||||
_db.updateRecordingStatus(recording.id, 'failed', errorReason: 'Erreur inattendue: $e');
|
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;
|
_currentRecording = null;
|
||||||
_ffmpegProcess = null;
|
_ffmpegProcess = null;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user