const fs = require('fs'); const path = require('path'); 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'); const storageManager = require('../data/storageManager'); const measureManager = require('../measure/measureManager'); const PROJECTS_DIR = path.join('.'); async function deleteUnfinishedVideos() { // Au démarrage du backend, supprimer les vidéos inachevées (donc en status 1) const unfinishedVideos = await db.query(` SELECT id FROM public.videos WHERE status = 0 OR status = 2 OR status = 3 `); for (const video of unfinishedVideos.rows) { try { await deleteVideoProject(video.id); console.log(`Deleted unfinished video with id: ${video.id}`); } catch (error) { console.error(`Error deleting unfinished video with id: ${video.id}`, error); } } } async function cleanVideoFiles() { //supprimer les fichiers vidéos qui ne sont pas associés à une vidéo de la base de données } deleteUnfinishedVideos(); async function createVideoProject(projectId, measurementIds, name, resolution, duration) { // insérer une nouvelle vidéo dans la base de données const status = 0; // 0 = en cours, 1 = terminé, 2 = erreur, 3 = en cours de création const query = 'INSERT INTO public.videos (project_id, measurement_ids, name, resolution, duration, status) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id'; const values = [projectId, measurementIds, name, resolution, duration, status]; const res = await db.query(query, values); console.log('New video created with id:', res.rows[0].id); return res.rows[0].id; } async function deleteVideoProject(videoId) { const query = 'DELETE FROM public.videos WHERE id = $1'; const values = [videoId]; const res = await db.query(query, values); console.log('Video deleted:', res.rows[0]); return res.rows[0]; } async function createVideoWithList(projectId, pathList, duration, videoId, res_width, res_height) { const tempFile = path.join('temp.txt'); let ffmpegProcess; let cleanupDone = false; try { // Configuration des chemins const workdir = path.join(PROJECTS_DIR, 'storage', projectId.toString()); if (!fs.existsSync(workdir)) { fs.mkdirSync(workdir, { recursive: true }); } // 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); return numA - numB; }); // Création du fichier temporaire fs.writeFileSync(tempFile, sortedImages.map(image => `file '${image}'`).join('\n')); // 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}_${firstImageId}_${lastImageId}-${timestamp}.mp4` ); // 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]); const scale = res_width && res_height ? `scale=${res_width}:${res_height}` : 'scale=854:480'; // Redimensionne la vidéo en 480p par défaut // Configuration de FFmpeg const ffmpegArgs = [ '-y', '-r', frameRate.toString(), '-f', 'concat', '-safe', '0', '-i', tempFile, '-vsync', 'vfr', '-pix_fmt', 'yuv420p', '-vf', scale, '-b:v', '1500k', // Force un bitrate vidéo de 1500 kbps (ajuste si nécessaire) outputVideo ]; ffmpegProcess = spawn('ffmpeg', ffmpegArgs, { stdio: ['ignore', 'ignore', 'pipe'] }); let lastUpdate = 0; const startTime = Date.now(); // É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 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); }); return outputVideo; } catch (error) { // Gestion des erreurs console.error('Error in video creation:', error); 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 if (!cleanupDone) { if (tempFile && fs.existsSync(tempFile)) { fs.unlinkSync(tempFile); } if (ffmpegProcess) { ffmpegProcess.kill(); } cleanupDone = true; } } } async function createVideo(projectId) { const tempFile = path.join('temp.txt'); try { // Trouver tous les fichiers image pour le projet donné const workdir = path.join(PROJECTS_DIR, 'storage', `${projectId}`); const dir = path.join(PROJECTS_DIR, 'storage', `${projectId}`, 'images'); console.log('dir:', dir); const images = storageManager.scanAllImages(dir); console.log('images:', images); // Trier les images numériquement const sortedImages = images.sort((a, b) => { const numA = parseInt(path.basename(a).match(/\d+/)[0], 10); const numB = parseInt(path.basename(b).match(/\d+/)[0], 10); return numA - numB; }); // En déduire l'id de la première et dernière image utilisée const firstImageId = parseInt(path.basename(sortedImages[0]).match(/\d+/)[0], 10); const lastImageId = parseInt(path.basename(sortedImages[sortedImages.length - 1]).match(/\d+/)[0], 10); console.log('firstImageId:', firstImageId); console.log('lastImageId:', lastImageId); // Créer un fichier temporaire pour la liste des images fs.writeFileSync(tempFile, sortedImages.map(image => `file '${image}'`).join('\n')); const frameRate = 10; const outputVideo = path.join(workdir, 'video.mp4'); // Commande ffmpeg pour créer la vidéo const ffmpegCommand = `ffmpeg -r ${frameRate} -f concat -safe 0 -i ${tempFile} -vsync vfr -pix_fmt yuv420p ${outputVideo}`; console.log('Running ffmpeg command:', ffmpegCommand); spawn(ffmpegCommand); console.log('Video created successfully:', outputVideo); } catch (error) { console.error('Error creating video:', error); serverError(error); } finally { // Supprimer le fichier temporaire if (fs.existsSync(tempFile)) { fs.unlinkSync(tempFile); console.log('Temporary file deleted:', tempFile); } } } async function updateVideoFile(videoId, video_file) { const query = 'UPDATE public.videos SET video_file = $2 WHERE id = $1 RETURNING *'; const values = [videoId, video_file]; const res = await db.query(query, values); console.log('Video updated:', res.rows[0]); return res.rows[0]; } module.exports = { createVideo, createVideoWithList, createVideoProject, deleteVideoProject, updateVideoFile };