mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-24 15:38:04 +02:00
feat(recording): add ffmpeg logs writing and viewing in UI
This commit is contained in:
1 parent
abbac75926
commit
966311f560
3 files changed
+110
-2
No files matched your search
@@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:io';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:shelf/shelf.dart';
|
import 'package:shelf/shelf.dart';
|
||||||
import 'package:shelf_router/shelf_router.dart';
|
import 'package:shelf_router/shelf_router.dart';
|
||||||
@@ -85,6 +86,32 @@ class RecordingsApi {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Consulter les logs d'un enregistrement
|
||||||
|
router.get('/<id>/log', (Request request, String id) async {
|
||||||
|
final recording = _db.getRecordingById(id);
|
||||||
|
|
||||||
|
if (recording == null) {
|
||||||
|
return Response.notFound(json.encode({'error': 'Enregistrement non trouvé'}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recording.filePath == null) {
|
||||||
|
return Response.notFound(json.encode({'error': 'Aucun fichier ni log associé pour le moment.'}));
|
||||||
|
}
|
||||||
|
|
||||||
|
final logFilePath = recording.filePath!.replaceAll('.mp4', '.log');
|
||||||
|
final logFile = File(logFilePath);
|
||||||
|
|
||||||
|
if (!await logFile.exists()) {
|
||||||
|
return Response.notFound(json.encode({'error': 'Le fichier de log est introuvable.'}));
|
||||||
|
}
|
||||||
|
|
||||||
|
final logs = await logFile.readAsString();
|
||||||
|
return Response.ok(
|
||||||
|
json.encode({'logs': logs}),
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -118,21 +118,43 @@ class RecordingScheduler {
|
|||||||
|
|
||||||
_ffmpegProcess = await Process.start('ffmpeg', args);
|
_ffmpegProcess = await Process.start('ffmpeg', args);
|
||||||
|
|
||||||
|
// Création d'un fichier de log dédié pour FFmpeg
|
||||||
|
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');
|
||||||
|
logSink.writeln('Commande: ffmpeg ${args.join(' ')}\n');
|
||||||
|
|
||||||
|
// Rediriger la sortie de ffmpeg vers ce fichier
|
||||||
|
_ffmpegProcess!.stdout.listen((event) {
|
||||||
|
logSink.add(event);
|
||||||
|
});
|
||||||
|
_ffmpegProcess!.stderr.listen((event) {
|
||||||
|
logSink.add(event); // FFmpeg écrit beaucoup d'infos (dont la progression) sur stderr
|
||||||
|
});
|
||||||
|
|
||||||
// Enregistrer le chemin du fichier dans la BDD
|
// Enregistrer le chemin du fichier dans la BDD
|
||||||
_db.updateRecordingStatus(recording.id, 'recording', filePath: filePath);
|
_db.updateRecordingStatus(recording.id, 'recording', filePath: filePath);
|
||||||
|
|
||||||
// Écouter de manière asynchrone la fin du processus FFmpeg
|
// Écouter de manière asynchrone la fin du processus FFmpeg
|
||||||
_ffmpegProcess!.exitCode.then((exitCode) {
|
_ffmpegProcess!.exitCode.then((exitCode) async {
|
||||||
if (_currentRecording?.id == recording.id) {
|
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é (SIGKILL/SIGTERM) qui est attendu
|
if (exitCode == 0 || exitCode == 255) { // 255 est souvent renvoyé lors d'un arrêt forcé (SIGKILL/SIGTERM) qui est attendu
|
||||||
print('[RecordingScheduler] Enregistrement terminé avec succès (${recording.title})');
|
print('[RecordingScheduler] Enregistrement terminé avec succès (${recording.title})');
|
||||||
_db.updateRecordingStatus(recording.id, 'completed');
|
_db.updateRecordingStatus(recording.id, 'completed');
|
||||||
} else {
|
} else {
|
||||||
print('[RecordingScheduler] Erreur FFmpeg (code: $exitCode) pour l\'enregistrement ${recording.title}');
|
print('[RecordingScheduler] Erreur FFmpeg (code: $exitCode) pour l\'enregistrement ${recording.title}');
|
||||||
_db.updateRecordingStatus(recording.id, 'failed', errorReason: 'Erreur FFmpeg code $exitCode');
|
_db.updateRecordingStatus(recording.id, 'failed', errorReason: 'Erreur FFmpeg code $exitCode. Voir logs.');
|
||||||
}
|
}
|
||||||
_currentRecording = null;
|
_currentRecording = null;
|
||||||
_ffmpegProcess = null;
|
_ffmpegProcess = null;
|
||||||
|
} else {
|
||||||
|
await logSink.close();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -71,6 +71,59 @@ class _RecordingsTabState extends State<RecordingsTab> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _showLogs(String id, String title) async {
|
||||||
|
try {
|
||||||
|
final response = await http.get(Uri.parse('/api/recordings/$id/log'));
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final data = json.decode(response.body);
|
||||||
|
final logs = data['logs'] ?? 'Aucun log trouvé.';
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
backgroundColor: const Color(0xFF1E1E1E),
|
||||||
|
title: Text('Journaux pour "$title"', style: const TextStyle(color: Colors.white)),
|
||||||
|
content: SizedBox(
|
||||||
|
width: double.maxFinite,
|
||||||
|
height: 400,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Text(
|
||||||
|
logs,
|
||||||
|
style: GoogleFonts.firaCode(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.greenAccent,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: const Text('Fermer', style: TextStyle(color: Colors.blueAccent)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (mounted) {
|
||||||
|
final data = json.decode(response.body);
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(data['error'] ?? 'Erreur lors de la récupération des logs'), backgroundColor: Colors.orangeAccent),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Impossible de récupérer les logs: $e'), backgroundColor: Colors.red),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Color _getStatusColor(String status) {
|
Color _getStatusColor(String status) {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'recording':
|
case 'recording':
|
||||||
@@ -203,8 +256,14 @@ class _RecordingsTabState extends State<RecordingsTab> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.description_outlined, color: Colors.blueAccent),
|
||||||
|
tooltip: 'Voir les logs',
|
||||||
|
onPressed: () => _showLogs(rec['id'], rec['title'] ?? 'Inconnu'),
|
||||||
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.delete_outline, color: Colors.white54),
|
icon: const Icon(Icons.delete_outline, color: Colors.white54),
|
||||||
|
tooltip: 'Supprimer',
|
||||||
onPressed: () => _deleteRecording(rec['id']),
|
onPressed: () => _deleteRecording(rec['id']),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in new issue
Block a user