From c3e78b248fb7efa05b33b0597b80f2c986df8f75 Mon Sep 17 00:00:00 2001 From: dakerboul Date: Thu, 13 Mar 2025 11:50:31 +0100 Subject: [PATCH] =?UTF-8?q?Ajouter=20une=20route=20pour=20r=C3=A9cup=C3=A9?= =?UTF-8?q?rer=20la=20progression=20de=20la=20cr=C3=A9ation=20de=20vid?= =?UTF-8?q?=C3=A9os=20et=20am=C3=A9liorer=20la=20gestion=20des=20erreurs?= =?UTF-8?q?=20dans=20la=20fonction=20createVideoWithList?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- routes/videoRoutes.js | 40 ++++++++++ src/video/videoManager.js | 155 ++++++++++++++++++++++++++++---------- 2 files changed, 155 insertions(+), 40 deletions(-) diff --git a/routes/videoRoutes.js b/routes/videoRoutes.js index a8e2bf3..776650b 100644 --- a/routes/videoRoutes.js +++ b/routes/videoRoutes.js @@ -202,6 +202,46 @@ router.get('/videos/reset/:video_id', (req, res) => { }); }); +router.get('/videos/progress/:video_id', async (req, res) => { + try { + const result = await db.query(` + SELECT + progress, + EXTRACT(EPOCH FROM (NOW() - started_at)) as elapsed, + eta, + status + FROM public.videos + WHERE id = $1 + `, [req.params.video_id]); + + if (result.rows.length === 0) { + return res.status(404).json({ error: 'Vidéo non trouvée' }); + } + + const video = result.rows[0]; + res.json({ + progress: video.progress, + elapsed: video.elapsed, + eta: video.eta, + status: this.getStatusLabel(video.status) + }); + + } catch (error) { + console.error(error); + res.status(500).json({ error: 'Erreur de récupération' }); + } +}); + +function getStatusLabel(status) { + const statusMap = { + 0: 'En attente', + 1: 'Terminé', + 2: 'Échec', + 3: 'En cours' + }; + return statusMap[status] || 'Inconnu'; +} + router.get('/cat', (_, res) => { const videoPath = dbTester.getCatVideo(); diff --git a/src/video/videoManager.js b/src/video/videoManager.js index 7afa6cf..b4df2e9 100644 --- a/src/video/videoManager.js +++ b/src/video/videoManager.js @@ -4,6 +4,8 @@ const { execSync } = require('child_process'); const { exec } = require('child_process'); const util = require('util'); const execPromise = util.promisify(exec); +const { spawn } = require('child_process'); +let globalProgress = {}; const serverError = require('../../utils/serverError'); const db = require('../../db'); @@ -32,18 +34,17 @@ async function deleteVideoProject(videoId) { async function createVideoWithList(projectId, pathList, duration, videoId) { const tempFile = path.join('temp.txt'); - let ffmpegSuccess = false; + let ffmpegProcess; + let cleanupDone = false; try { - const workdir = path.join(PROJECTS_DIR, 'storage', `${projectId}`); - const dir = path.join(PROJECTS_DIR, 'storage', `${projectId}`, 'images'); - - // Vérification de l'existence du répertoire + // Configuration des chemins + const workdir = path.join(PROJECTS_DIR, 'storage', projectId.toString()); if (!fs.existsSync(workdir)) { fs.mkdirSync(workdir, { recursive: true }); } - // Triage des images + // Tri des images const sortedImages = pathList.sort((a, b) => { const numA = parseInt(path.basename(a).match(/\d+/)[0], 10); const numB = parseInt(path.basename(b).match(/\d+/)[0], 10); @@ -53,65 +54,139 @@ async function createVideoWithList(projectId, pathList, duration, videoId) { // Création du fichier temporaire fs.writeFileSync(tempFile, sortedImages.map(image => `file '${image}'`).join('\n')); - // Calcul du frame rate - const frameRate = Math.ceil(sortedImages.length / parseInt(duration)); - - // Génération du nom de fichier + // Calcul des paramètres vidéo + const totalFrames = sortedImages.length; + const frameRate = Math.ceil(totalFrames / parseInt(duration)); const timestamp = Date.now(); + const firstImageId = path.basename(sortedImages[0]).match(/\d+/)[0]; + const lastImageId = path.basename(sortedImages[sortedImages.length - 1]).match(/\d+/)[0]; const outputVideo = path.join( workdir, - `${projectId}_${path.basename(sortedImages[0], path.extname(sortedImages[0]))}_${path.basename(sortedImages[sortedImages.length - 1], path.extname(sortedImages[sortedImages.length - 1]))}-${timestamp}.mp4` + `${projectId}_${firstImageId}_${lastImageId}-${timestamp}.mp4` ); - // Commande FFmpeg - const ffmpegCommand = [ - 'ffmpeg', - '-y', // Overwrite output file - '-r', frameRate, + // Mise à jour initiale de la base de données + await db.query(` + UPDATE public.videos + SET + status = 3, + progress = 0, + started_at = NOW(), + updated_at = NOW(), + eta = NULL + WHERE id = $1 + `, [videoId]); + + // Configuration de FFmpeg + const ffmpegArgs = [ + '-y', + '-r', frameRate.toString(), '-f', 'concat', '-safe', '0', '-i', tempFile, '-vsync', 'vfr', '-pix_fmt', 'yuv420p', outputVideo - ].join(' '); + ]; - console.log(`Exécution de la commande FFmpeg: ${ffmpegCommand}`); - const { stderr } = await execPromise(ffmpegCommand); + ffmpegProcess = spawn('ffmpeg', ffmpegArgs, { + stdio: ['ignore', 'ignore', 'pipe'] + }); - // Vérification des erreurs FFmpeg - if (stderr.includes('Error') || stderr.includes('failed')) { - throw new Error(`Erreur FFmpeg: ${stderr}`); - } + let lastUpdate = 0; + const startTime = Date.now(); - ffmpegSuccess = true; + // Écoute de la sortie d'erreur pour la progression + ffmpegProcess.stderr.on('data', (data) => { + const output = data.toString(); + const frameMatch = output.match(/frame=\s*(\d+)/); + + if (frameMatch) { + const currentFrame = parseInt(frameMatch[1], 10); + const progress = Math.min((currentFrame / totalFrames) * 100, 99.99); + const now = Date.now(); + + // Calcul de l'ETA + const elapsedSeconds = (now - startTime) / 1000; + const eta = elapsedSeconds / (currentFrame / totalFrames) - elapsedSeconds; - // Mise à jour de la base de données - const updateStatusRes = await db.query( - 'UPDATE public.videos SET status = $1, video_file = $2 WHERE id = $3 RETURNING *', - [1, outputVideo, videoId] - ); + // Mise à jour max toutes les 500ms + if (now - lastUpdate > 500) { + db.query(` + UPDATE public.videos + SET + progress = $1, + eta = $2, + updated_at = NOW() + WHERE id = $3 + `, [progress, Math.round(eta), videoId]).catch(console.error); + + console.log('Progress:', progress.toFixed(2), '%, ETA:', eta.toFixed(0), 's'); + lastUpdate = now; + } + } + }); + + // Attente de la fin du processus + await new Promise((resolve, reject) => { + ffmpegProcess.on('close', async (code) => { + if (code === 0) { + try { + // Mise à jour finale + await db.query(` + UPDATE public.videos + SET + status = 1, + progress = 100, + eta = 0, + video_file = $1, + updated_at = NOW() + WHERE id = $2 + `, [outputVideo, videoId]); + resolve(); + } catch (e) { + reject(e); + } + } else { + reject(new Error(`FFmpeg process exited with code ${code}`)); + } + }); + + ffmpegProcess.on('error', reject); + }); - console.log('Vidéo et statut mis à jour:', updateStatusRes.rows[0]); return outputVideo; } catch (error) { - console.error('Erreur lors de la création vidéo:', error); + // Gestion des erreurs + console.error('Error in video creation:', error); - // Mise à jour du statut en erreur - if (ffmpegSuccess) { - await db.query( - 'UPDATE public.videos SET status = $1 WHERE id = $2', - [3, videoId] // 3 = statut erreur - ); + try { + await db.query(` + UPDATE public.videos + SET + status = 2, + progress = 0, + eta = 0, + updated_at = NOW() + WHERE id = $1 + `, [videoId]); + } catch (dbError) { + console.error('Database update error:', dbError); } throw error; } finally { - // Nettoyage du fichier temporaire - if (fs.existsSync(tempFile)) { - fs.unlinkSync(tempFile); + // Nettoyage + if (!cleanupDone) { + if (tempFile && fs.existsSync(tempFile)) { + fs.unlinkSync(tempFile); + } + if (ffmpegProcess) { + ffmpegProcess.kill(); + } + cleanupDone = true; } } }