Fix broken Timelapse Video Build if too many frames

cron-as-worker-process
Moe 2022-06-27 00:30:22 -07:00
parent 4b42dd6ed4
commit ee556be4ef
1 changed files with 203 additions and 101 deletions

View File

@ -154,9 +154,166 @@ module.exports = function(s,config,lang,app,io){
}
})
}
function createVideoFromTimelapse(timelapseFrames,framesPerSecond){
function splitArrayIntoMultiple(bigarray,size){
size = size || 80;
var arrayOfArrays = [];
for (var i=0; i<bigarray.length; i+=size) {
arrayOfArrays.push(bigarray.slice(i,i+size));
}
return arrayOfArrays
}
async function createTemporaryInputFile(frames,concatListFile){
const concatFiles = []
const fileList = []
frames.forEach(function(frame,frameNumber){
var selectedDate = frame.filename.split('T')[0]
var fileLocationMid = `${frame.ke}/${frame.mid}_timelapse/${selectedDate}/`
frame.details = s.parseJSON(frame.details)
var fileLocation
if(frame.details.dir){
fileLocation = `${s.checkCorrectPathEnding(frame.details.dir)}`
}else{
fileLocation = `${s.dir.videos}`
}
fileLocation = `${fileLocation}${fileLocationMid}${frame.filename}`
try{
fs.statSync(fileLocation)
concatFiles.push(`file '${fileLocation}'`)
fileList.push(`${fileLocation}`)
}catch(err){
s.debugLog(`Failed to read frame for Timelapse build`)
}
})
await fs.promises.writeFile(concatListFile,concatFiles.join('\n'))
return fileList
}
async function createTemporaryInputFileForStitched(videosPathsList,concatListFile){
const concatFiles = []
const fileList = []
videosPathsList.forEach(function(videoPath){
try{
fs.statSync(videoPath)
concatFiles.push(`file '${videoPath}'`)
fileList.push(`${videoPath}`)
}catch(err){
s.debugLog(`Failed to read segment for Timelapse build`)
}
})
s.debugLog(concatFiles)
await fs.promises.writeFile(concatListFile,concatFiles.join('\n'))
return fileList
}
function buildVideoSegmentFromFrames(options){
return new Promise((resolve,reject) => {
const frames = options.frames
const ke = frames[0].ke
const mid = frames[0].mid
const concatListFile = options.listFile
createTemporaryInputFile(frames,concatListFile).then((framesAccepted) => {
var completionTimeout
const framesPerSecond = options.fps
const finalMp4OutputLocation = options.output
const onPercentChange = options.onPercentChange
const numberOfFrames = framesAccepted.length
const commandString = `-y -threads 1 -re -f concat -safe 0 -r ${framesPerSecond} -i "${concatListFile}" -q:v 1 -c:v libx264 -preset ultrafast -r ${framesPerSecond} "${finalMp4OutputLocation}"`
s.debugLog("ffmpeg",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){
const text = data.toString()
if(text.startsWith('frame=')){
const currentFrame = parseInt(text.split(/(\s+)/)[2])
const percent = (currentFrame / numberOfFrames * 100).toFixed(1)
onPercentChange(percent,currentFrame)
}
clearTimeout(completionTimeout)
completionTimeout = setTimeout(function(){
s.debugLog('videoBuildProcess completionTimeout',finalMp4OutputLocation)
processKill(videoBuildProcess)
},20000)
})
videoBuildProcess.on('exit',async function(data){
clearTimeout(completionTimeout)
resolve()
await fs.promises.unlink(concatListFile)
})
})
})
}
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
const ke = frames[0].ke
const mid = frames[0].mid
const finalFileName = options.finalFileName
const concatListFile = options.listFile
const framesPerSecond = options.fps
const finalMp4OutputLocation = options.output
const onPercentChange = options.onPercentChange
const frameChunks = splitArrayIntoMultiple(frames,80)
const numberOfSets = frameChunks.length
const filePathsList = []
for (let i = 0; i < numberOfSets; i++) {
var frameSet = frameChunks[i]
var numberOfFrames = frameSet.length
var segmentFileOutput = `${s.dir.streams}${ke}/${mid}/${s.gid(10)}.mp4`
filePathsList.push(segmentFileOutput)
await buildVideoSegmentFromFrames({
frames: frameSet,
listFile: `${concatListFile}${i}`,
fps: framesPerSecond,
output: segmentFileOutput,
onPercentChange: (percent,currentFrame) => {
const overallPercent = ((percent / numberOfSets) + (i * (100 / numberOfSets))).toFixed(1);
s.tx({
f: 'timelapse_build_percent',
ke: ke,
mid: mid,
name: finalFileName,
percent: overallPercent,
},'GRP_'+ke);
if(percent == 100){
s.debugLog('videoBuildProcess 100%',finalMp4OutputLocation)
}
s.debugLog(`Piece ${i}`,`${currentFrame} / ${numberOfFrames}`,`${percent}%`)
},
})
}
s.debugLog('videoBuildProcess Stitching...',finalMp4OutputLocation)
await createTemporaryInputFileForStitched(filePathsList,concatListFile)
await stitchMp4Files({
listFile: concatListFile,
output: finalMp4OutputLocation,
})
await fs.promises.unlink(concatListFile)
for (let i = 0; i < filePathsList; i++) {
var segmentFileOutput = filePathsList[i]
await fs.promises.unlink(segmentFileOutput)
}
s.debugLog('videoBuildProcess Stitching Complete!',finalMp4OutputLocation)
}
async function createVideoFromTimelapse(timelapseFrames,framesPerSecond){
s.debugLog("Building Timelapse Frames Video",timelapseFrames.length)
framesPerSecond = !isNaN(framesPerSecond) ? framesPerSecond : parseInt(framesPerSecond) || 2
const frames = timelapseFrames.reverse()
const numberOfFrames = timelapseFrames.length
@ -167,7 +324,6 @@ module.exports = function(s,config,lang,app,io){
const finalMp4OutputLocation = `${s.dir.fileBin}${ke}/${mid}/${finalFileName}`
const finalFileAlreadyExist = fs.existsSync(finalMp4OutputLocation)
const concatListFile = `${s.dir.streams}${ke}/${mid}/mergeJpegs_${finalFileName}.txt`
const concatFiles = []
const response = {
ok: false,
ke: ke,
@ -187,42 +343,18 @@ module.exports = function(s,config,lang,app,io){
response.msg = lang['Already exists']
return response
}
var createLocation
frames.forEach(function(frame,frameNumber){
var selectedDate = frame.filename.split('T')[0]
var fileLocationMid = `${frame.ke}/${frame.mid}_timelapse/${selectedDate}/`
frame.details = s.parseJSON(frame.details)
var fileLocation
if(frame.details.dir){
fileLocation = `${s.checkCorrectPathEnding(frame.details.dir)}`
}else{
fileLocation = `${s.dir.videos}`
}
fileLocation = `${fileLocation}${fileLocationMid}${frame.filename}`
concatFiles.push(`file '${fileLocation}'`)
if(frameNumber === 0){
createLocation = fileLocationMid
}
})
if(concatFiles.length < framesPerSecond){
if(frames.length < framesPerSecond){
response.msg = lang.notEnoughFramesText1
return response
}
fs.writeFileSync(concatListFile,concatFiles.join('\n'))
activeMonitor.buildingTimelapseVideo = response
var currentFile = 0
var completionTimeout
const commandString = `-y -f concat -safe 0 -r ${framesPerSecond} -i "${concatListFile}" -q:v 1 -c:v libx264 -r ${framesPerSecond} "${finalMp4OutputLocation}"`
s.debugLog("ffmpeg",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){
const text = data.toString()
if(text.startsWith('frame=')){
const currentFrame = parseInt(text.split(/(\s+)/)[2])
const percent = (currentFrame / numberOfFrames * 100).toFixed(1)
chunkFramesAndBuildMultipleVideosThenSticth({
frames: frames,
listFile: concatListFile,
fps: framesPerSecond,
output: finalMp4OutputLocation,
finalFileName: finalFileName,
onPercentChange: (percent,currentFrame) => {
s.tx({
f: 'timelapse_build_percent',
ke: ke,
@ -230,53 +362,47 @@ module.exports = function(s,config,lang,app,io){
name: finalFileName,
percent: percent,
},'GRP_'+ke);
if(percent === 100){
if(percent == 100){
s.debugLog('videoBuildProcess 100%',finalMp4OutputLocation)
clearTimeout(completionTimeout)
}
s.debugLog('Completion',`${currentFrame} / ${numberOfFrames}`,`${percent}%`)
}
clearTimeout(completionTimeout)
completionTimeout = setTimeout(function(){
s.debugLog('videoBuildProcess completionTimeout',finalMp4OutputLocation)
processKill(videoBuildProcess)
},60000)
})
videoBuildProcess.on('exit',function(data){
},
}).then(async () => {
// videoBuildProcess exit
s.debugLog('videoBuildProcess exit',finalMp4OutputLocation)
const timeNow = new Date()
setTimeout(async () => {
const fileStats = await fs.promises.stat(finalMp4OutputLocation)
const details = {
video: true,
}
s.knexQuery({
action: "insert",
table: "Files",
insert: {
ke: ke,
mid: mid,
details: s.s(details),
name: finalFileName,
size: fileStats.size,
time: timeNow,
}
})
s.setDiskUsedForGroup(ke,fileStats.size / 1048576,'fileBin')
s.purgeDiskForGroup(ke)
s.tx({
f: 'fileBin_item_added',
const fileStats = await fs.promises.stat(finalMp4OutputLocation)
const details = {
video: true,
start: frames[0].time,
end: frames[frames.length - 1].time,
}
s.knexQuery({
action: "insert",
table: "Files",
insert: {
ke: ke,
mid: mid,
details: details,
details: s.s(details),
name: finalFileName,
size: fileStats.size,
time: timeNow,
timelapseVideo: true,
},'GRP_'+ke);
delete(activeMonitor.buildingTimelapseVideo)
s.debugLog("Timelapse Frames Video Done!",finalMp4OutputLocation)
},25000)
}
})
s.setDiskUsedForGroup(ke,fileStats.size / 1048576,'fileBin')
s.purgeDiskForGroup(ke)
s.tx({
f: 'fileBin_item_added',
ke: ke,
mid: mid,
details: details,
name: finalFileName,
size: fileStats.size,
time: timeNow,
timelapseVideo: true,
},'GRP_'+ke);
delete(activeMonitor.buildingTimelapseVideo)
s.debugLog("Timelapse Frames Video Done!",finalMp4OutputLocation)
})
response.ok = true
response.msg = lang.Building
@ -322,31 +448,7 @@ module.exports = function(s,config,lang,app,io){
rowType: 'frames',
endIsStartTo: true
},(response) => {
var isMp4Call = !!(req.query.mp4 || (req.params.date && typeof req.params.date === 'string' && req.params.date.indexOf('.') > -1))
if(isMp4Call && response.frames[0]){
s.createVideoFromTimelapse(response.frames,req.query.fps,function(response){
if(response.fileExists){
if(req.query.download){
res.setHeader('Content-Type', 'video/mp4')
s.streamMp4FileOverHttp(response.fileLocation,req,res)
}else{
s.closeJsonResponse(res,{
ok : response.ok,
fileExists : response.fileExists,
msg : response.msg,
})
}
}else{
s.closeJsonResponse(res,{
ok : response.ok,
fileExists : response.fileExists,
msg : response.msg,
})
}
})
}else{
s.closeJsonResponse(res,response.frames)
}
s.closeJsonResponse(res,response.frames)
})
},res,req);
});
@ -392,14 +494,14 @@ module.exports = function(s,config,lang,app,io){
columns: "*",
table: "Timelapse Frames",
where: frames
},(err,r) => {
},async (err,r) => {
if(r.length === 0){
s.closeJsonResponse(res,{
ok: false
})
return
}
const buildResponse = createVideoFromTimelapse(r.reverse(),s.getPostData(req, 'fps'))
const buildResponse = await createVideoFromTimelapse(r.reverse(),s.getPostData(req, 'fps'))
s.closeJsonResponse(res,{
ok : buildResponse.ok,
filename : buildResponse.filename,
@ -530,9 +632,9 @@ module.exports = function(s,config,lang,app,io){
groups[frame.ke][frame.mid].push(frame)
})
Object.keys(groups).forEach(function(groupKey){
Object.keys(groups[groupKey]).forEach(function(monitorId){
Object.keys(groups[groupKey]).forEach(async function(monitorId){
var frameSet = groups[groupKey][monitorId]
createVideoFromTimelapse(frameSet,30)
await createVideoFromTimelapse(frameSet,30)
})
})
})