diff --git a/languages/en_CA.json b/languages/en_CA.json index de8ed5a0..139ee804 100644 --- a/languages/en_CA.json +++ b/languages/en_CA.json @@ -485,7 +485,7 @@ "Delete Filter": "Delete Filter", "confirmDeleteFilter": "Do you want to delete this filter? You cannot recover it.", "Fix Video": "Fix Video", - "FixVideoMsg": "Do you want to fix this video? You cannot undo this action.", + "FixVideoMsg": "Do you want to fix this video? This will create a new file and overwrite the old one. You cannot undo this action.", "DeleteVideoMsg": "Do you want to delete this video? You cannot recover it.", "DeleteThisMsg": "Do you want to delete this? You cannot recover it.", "DeleteTheseMsg": "Do you want to delete these? You cannot recover them.", diff --git a/libs/timelapse.js b/libs/timelapse.js index 0cfaeb71..3f8c3869 100644 --- a/libs/timelapse.js +++ b/libs/timelapse.js @@ -17,6 +17,9 @@ module.exports = function(s,config,lang,app,io){ const { processKill, } = require('./monitor/utils.js')(s,config,lang) + const { + stitchMp4Files, + } = require('./video/utils.js')(s,config,lang) const timelapseFramesCache = {} const timelapseFramesCacheTimeouts = {} s.getTimelapseFrameDirectory = function(e){ @@ -248,24 +251,6 @@ module.exports = function(s,config,lang,app,io){ }) }) } - async function stitchMp4Files(options){ - return new Promise((resolve,reject) => { - const concatListFile = options.listFile - const finalMp4OutputLocation = options.output - const commandString = `-y -threads 1 -f concat -safe 0 -i "${concatListFile}" -c:v copy -an -preset ultrafast "${finalMp4OutputLocation}"` - s.debugLog("stitchMp4Files",commandString) - const videoBuildProcess = spawn(config.ffmpegDir,splitForFFPMEG(commandString)) - videoBuildProcess.stdout.on('data',function(data){ - s.debugLog('stdout',finalMp4OutputLocation,data.toString()) - }) - videoBuildProcess.stderr.on('data',function(data){ - s.debugLog('stderr',finalMp4OutputLocation,data.toString()) - }) - videoBuildProcess.on('exit',async function(data){ - resolve() - }) - }) - } async function chunkFramesAndBuildMultipleVideosThenSticth(options){ // a single video with too many frames makes the video unplayable, this is the fix. const frames = options.frames @@ -311,10 +296,10 @@ module.exports = function(s,config,lang,app,io){ listFile: concatListFile, output: finalMp4OutputLocation, }) - await fs.promises.unlink(concatListFile) - for (let i = 0; i < filePathsList; i++) { + await fs.promises.rm(concatListFile) + for (let i = 0; i < filePathsList.length; i++) { var segmentFileOutput = filePathsList[i] - await fs.promises.unlink(segmentFileOutput) + await fs.promises.rm(segmentFileOutput) } s.debugLog('videoBuildProcess Stitching Complete!',finalMp4OutputLocation) } diff --git a/libs/video/utils.js b/libs/video/utils.js index 2332c04c..ce6e58c8 100644 --- a/libs/video/utils.js +++ b/libs/video/utils.js @@ -1,6 +1,9 @@ const fs = require('fs') const { spawn } = require('child_process') module.exports = (s,config,lang) => { + const { + splitForFFPMEG, + } = require('../ffmpeg/utils.js')(s,config,lang) // orphanedVideoCheck : new function const checkIfVideoIsOrphaned = (monitor,videosDirectory,filename) => { const response = {ok: true} @@ -64,10 +67,10 @@ module.exports = (s,config,lang) => { let listing = spawn('sh',[tempDirectory + 'orphanCheck.sh']) // const onData = options.onData ? options.onData : () => {} const onError = options.onError ? options.onError : s.systemLog - const onExit = () => { + const onExit = async () => { try{ listing.kill('SIGTERM') - fs.unlink(tempDirectory + 'orphanCheck.sh',() => {}) + await fs.promises.rm(tempDirectory + 'orphanCheck.sh') }catch(err){ s.debugLog(err) } @@ -235,7 +238,77 @@ module.exports = (s,config,lang) => { }); return videoSelectResponse } + async function stitchMp4Files(options){ + return new Promise((resolve,reject) => { + const concatListFile = options.listFile + const finalMp4OutputLocation = options.output + const commandString = `-y -threads 1 -f concat -safe 0 -i "${concatListFile}" -c:v copy -an -preset ultrafast "${finalMp4OutputLocation}"` + s.debugLog("stitchMp4Files",commandString) + const videoBuildProcess = spawn(config.ffmpegDir,splitForFFPMEG(commandString)) + videoBuildProcess.stdout.on('data',function(data){ + s.debugLog('stdout',finalMp4OutputLocation,data.toString()) + }) + videoBuildProcess.stderr.on('data',function(data){ + s.debugLog('stderr',finalMp4OutputLocation,data.toString()) + }) + videoBuildProcess.on('exit',async function(data){ + resolve() + }) + }) + } + const fixingAlready = {} + async function reEncodeVideoAndReplace(videoRow){ + return new Promise((resolve,reject) => { + const response = {ok: true} + const fixingId = `${videoRow.ke}${videoRow.mid}${videoRow.time}` + if(fixingAlready[fixingId]){ + response.ok = false + response.msg = lang['Already Processing'] + resolve(response) + }else{ + const filename = s.formattedTime(videoRow.time)+'.'+videoRow.ext + const tempFilename = s.formattedTime(videoRow.time)+'.reencoding.'+videoRow.ext + const videoFolder = s.getVideoDirectory(videoRow) + const inputFilePath = `${videoFolder}${filename}` + const outputFilePath = `${videoFolder}${tempFilename}` + const commandString = `-y -threads 1 -re -i "${inputFilePath}" -c:v copy -c:a copy -preset ultrafast "${outputFilePath}"` + fixingAlready[fixingId] = true + const videoBuildProcess = spawn(config.ffmpegDir,splitForFFPMEG(commandString)) + videoBuildProcess.stdout.on('data',function(data){ + s.debugLog('stdout',outputFilePath,data.toString()) + }) + videoBuildProcess.stderr.on('data',function(data){ + s.debugLog('stderr',outputFilePath,data.toString()) + }) + videoBuildProcess.on('exit',async function(data){ + fixingAlready[fixingId] = false + try{ + function failed(err){ + response.ok = false + response.err = err + resolve(response) + } + const newFileStats = await fs.promises.stat(outputFilePath) + await fs.promises.rm(inputFilePath) + let readStream = fs.createReadStream(outputFilePath); + let writeStream = fs.createWriteStream(inputFilePath); + readStream.pipe(writeStream); + writeStream.on('finish', async () => { + resolve(response) + await fs.promises.rm(outputFilePath) + }); + writeStream.on('error', failed); + readStream.on('error', failed); + }catch(err){ + failed() + } + }) + } + }) + } return { + reEncodeVideoAndReplace, + stitchMp4Files, orphanedVideoCheck: orphanedVideoCheck, scanForOrphanedVideos: scanForOrphanedVideos, cutVideoLength: cutVideoLength, diff --git a/libs/webServerPaths.js b/libs/webServerPaths.js index 8d26c141..6830ac78 100644 --- a/libs/webServerPaths.js +++ b/libs/webServerPaths.js @@ -31,6 +31,7 @@ module.exports = function(s,config,lang,app,io){ destroySubstreamProcess, } = require('./monitor/utils.js')(s,config,lang) const { + reEncodeVideoAndReplace, getVideosBasedOnTagFoundInMatrixOfAssociatedEvent, } = require('./video/utils.js')(s,config,lang) s.renderPage = function(req,res,paths,passables,callback){ @@ -1651,7 +1652,7 @@ module.exports = function(s,config,lang,app,io){ config.webPaths.apiPrefix+':auth/cloudVideos/:ke/:id/:file/:mode', config.webPaths.apiPrefix+':auth/cloudVideos/:ke/:id/:file/:mode/:f' ], function (req,res){ - var response = {ok:false}; + let response = { ok: false }; res.setHeader('Content-Type', 'application/json'); s.auth(req.params,function(user){ if(user.permissions.watch_videos==="0"||user.details.sub&&user.details.allmonitors!=='1'&&user.details.video_delete.indexOf(req.params.id)===-1){ @@ -1683,15 +1684,14 @@ module.exports = function(s,config,lang,app,io){ ['time','=',time] ], limit: 1 - },(err,r) => { + },async (err,r) => { if(r && r[0]){ r=r[0]; r.filename=s.formattedTime(r.time)+'.'+r.ext; var details = s.parseJSON(r.details) || {} switch(req.params.mode){ case'fix': - response.ok = true; - s.video('fix',r) + response = await reEncodeVideoAndReplace(r) break; case'status': r.f = 'video_edit' diff --git a/web/assets/js/bs5.videos.js b/web/assets/js/bs5.videos.js index b2b9e288..250fc5d4 100644 --- a/web/assets/js/bs5.videos.js +++ b/web/assets/js/bs5.videos.js @@ -583,7 +583,35 @@ $(document).ready(function(){ if(data.ok){ console.log('Video Deleted') }else{ - console.log('Video Not Deleted',data,deleteEndpoint) + console.log('Video Not Deleted',data,videoEndpoint) + } + }) + } + }); + return false; + }) + .on('click','.fix-video',function(e){ + e.preventDefault() + var el = $(this).parents('[data-mid]') + var monitorId = el.attr('data-mid') + var videoTime = el.attr('data-time') + var video = loadedVideosInMemory[`${monitorId}${videoTime}`] + var ext = video.filename.split('.') + ext = ext[ext.length - 1] + var videoEndpoint = getApiPrefix(`videos`) + '/' + video.mid + '/' + video.filename + $.confirm.create({ + title: lang["Fix Video"] + ' : ' + video.filename, + body: `${lang.FixVideoMsg}

`, + clickOptions: { + title: ' ' + lang.Fix, + class: 'btn-danger btn-sm' + }, + clickCallback: function(){ + $.getJSON(videoEndpoint + '/fix',function(data){ + if(data.ok){ + console.log('Video Fixed') + }else{ + console.log('Video Not Fixed',data,videoEndpoint) } }) } diff --git a/web/assets/js/bs5.videosTable.js b/web/assets/js/bs5.videosTable.js index 50f31534..1fdb9bee 100644 --- a/web/assets/js/bs5.videosTable.js +++ b/web/assets/js/bs5.videosTable.js @@ -115,6 +115,7 @@ $(document).ready(function(e){ + `,