2022-06-26 07:56:15 +00:00
|
|
|
const fs = require('fs')
|
|
|
|
const moment = require('moment')
|
|
|
|
const express = require('express')
|
|
|
|
const exec = require('child_process').exec;
|
|
|
|
const spawn = require('child_process').spawn;
|
|
|
|
const events = require('events');
|
2019-06-07 16:52:07 +00:00
|
|
|
module.exports = function(s,config,lang,app,io){
|
2021-11-27 01:47:20 +00:00
|
|
|
const {
|
|
|
|
sendTimelapseFrameToMasterNode,
|
|
|
|
} = require('./childNode/childUtils.js')(s,config,lang)
|
2022-06-26 07:56:15 +00:00
|
|
|
const {
|
2023-03-28 02:08:07 +00:00
|
|
|
splitForFFMPEG,
|
2022-06-26 07:56:15 +00:00
|
|
|
} = require('./ffmpeg/utils.js')(s,config,lang)
|
|
|
|
const {
|
2022-06-27 20:21:51 +00:00
|
|
|
getFileDirectory,
|
2022-06-26 07:56:15 +00:00
|
|
|
} = require('./basic/utils.js')(process.cwd(),config)
|
|
|
|
const {
|
|
|
|
processKill,
|
|
|
|
} = require('./monitor/utils.js')(s,config,lang)
|
2022-07-07 22:53:46 +00:00
|
|
|
const {
|
|
|
|
stitchMp4Files,
|
|
|
|
} = require('./video/utils.js')(s,config,lang)
|
2020-10-10 05:49:11 +00:00
|
|
|
const timelapseFramesCache = {}
|
|
|
|
const timelapseFramesCacheTimeouts = {}
|
2019-06-07 16:52:07 +00:00
|
|
|
s.getTimelapseFrameDirectory = function(e){
|
|
|
|
if(e.mid&&!e.id){e.id=e.mid}
|
|
|
|
s.checkDetails(e)
|
|
|
|
if(e.details&&e.details.dir&&e.details.dir!==''){
|
|
|
|
return s.checkCorrectPathEnding(e.details.dir)+e.ke+'/'+e.id+'_timelapse/'
|
|
|
|
}else{
|
|
|
|
return s.dir.videos+e.ke+'/'+e.id+'_timelapse/';
|
|
|
|
}
|
|
|
|
}
|
2021-05-20 03:18:19 +00:00
|
|
|
s.createTimelapseFrameAndInsert = function(e,location,filename,eventTime,frameDetails){
|
2019-06-07 16:52:07 +00:00
|
|
|
//e = monitor object
|
|
|
|
//location = file location
|
|
|
|
var filePath = location + filename
|
|
|
|
var fileStats = fs.statSync(filePath)
|
2021-05-20 03:18:19 +00:00
|
|
|
var details = Object.assign({},frameDetails || {})
|
2019-06-07 16:52:07 +00:00
|
|
|
if(e.details && e.details.dir && e.details.dir !== ''){
|
|
|
|
details.dir = e.details.dir
|
|
|
|
}
|
2021-11-27 01:47:20 +00:00
|
|
|
const timeNow = eventTime || new Date()
|
|
|
|
const queryInfo = {
|
2019-06-07 16:52:07 +00:00
|
|
|
ke: e.ke,
|
|
|
|
mid: e.id,
|
|
|
|
details: s.s(details),
|
|
|
|
filename: filename,
|
|
|
|
size: fileStats.size,
|
|
|
|
time: timeNow
|
|
|
|
}
|
2023-03-16 02:18:06 +00:00
|
|
|
if(config.childNodes.enabled === true && config.childNodes.mode === 'child' && config.childNodes.host && config.dropTimeLapseFrames != true){
|
2021-11-27 01:47:20 +00:00
|
|
|
var currentDate = s.formattedTime(timeNow,'YYYY-MM-DD')
|
|
|
|
const childNodeData = {
|
2019-06-07 16:52:07 +00:00
|
|
|
ke: e.ke,
|
|
|
|
mid: e.id,
|
2021-12-09 23:29:48 +00:00
|
|
|
time: currentDate,
|
2019-06-07 16:52:07 +00:00
|
|
|
filename: filename,
|
|
|
|
currentDate: currentDate,
|
|
|
|
queryInfo: queryInfo
|
2021-11-27 01:47:20 +00:00
|
|
|
}
|
|
|
|
sendTimelapseFrameToMasterNode(filePath,childNodeData)
|
2023-03-16 02:18:06 +00:00
|
|
|
}else if (config.dropTimeLapseFrames != true ){
|
2019-07-08 03:06:46 +00:00
|
|
|
s.insertTimelapseFrameDatabaseRow(e,queryInfo,filePath)
|
2019-06-07 16:52:07 +00:00
|
|
|
}
|
|
|
|
}
|
2019-07-08 03:06:46 +00:00
|
|
|
s.insertTimelapseFrameDatabaseRow = function(e,queryInfo,filePath){
|
2023-01-21 15:41:03 +00:00
|
|
|
const groupKey = e.ke
|
|
|
|
const theGroup = s.group[groupKey]
|
|
|
|
const frameDetails = Object.assign({},s.parseJSON(queryInfo.details) || {})
|
|
|
|
const storageId = e.details.dir
|
|
|
|
const storageIndex = theGroup.addStorageUse[storageId]
|
|
|
|
const fileSize = queryInfo.size / 1048576
|
2020-07-11 00:30:08 +00:00
|
|
|
s.knexQuery({
|
|
|
|
action: "insert",
|
|
|
|
table: "Timelapse Frames",
|
|
|
|
insert: queryInfo
|
|
|
|
})
|
2023-01-21 15:41:03 +00:00
|
|
|
if(storageIndex){
|
|
|
|
s.setDiskUsedForGroupAddStorage(groupKey,{
|
|
|
|
size: fileSize,
|
|
|
|
storageIndex: storageIndex
|
|
|
|
},'timelapseFrames')
|
|
|
|
}else{
|
|
|
|
s.setDiskUsedForGroup(groupKey, fileSize, 'timelapseFrames')
|
|
|
|
}
|
2020-08-07 07:54:56 +00:00
|
|
|
s.purgeDiskForGroup(e.ke)
|
2019-06-07 16:52:07 +00:00
|
|
|
s.onInsertTimelapseFrameExtensions.forEach(function(extender){
|
2019-07-08 03:06:46 +00:00
|
|
|
extender(e,queryInfo,filePath)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
s.onDeleteTimelapseFrameFromCloudExtensions = {}
|
|
|
|
s.onDeleteTimelapseFrameFromCloudExtensionsRunner = function(e,storageType,video){
|
|
|
|
// e = user
|
|
|
|
if(!storageType){
|
|
|
|
var videoDetails = JSON.parse(r.details)
|
|
|
|
videoDetails.type = videoDetails.type || 's3'
|
|
|
|
}
|
|
|
|
if(s.onDeleteTimelapseFrameFromCloudExtensions[storageType]){
|
|
|
|
s.onDeleteTimelapseFrameFromCloudExtensions[storageType](e,video,function(){
|
|
|
|
s.tx({
|
|
|
|
f: 'timelapse_frame_delete_cloud',
|
|
|
|
mid: e.mid,
|
|
|
|
ke: e.ke,
|
|
|
|
time: e.time,
|
|
|
|
end: e.end
|
|
|
|
},'GRP_'+e.ke);
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
2022-12-21 19:26:45 +00:00
|
|
|
s.deleteTimelapseFrameFromCloud = function(e,cloudType){
|
2019-07-08 03:06:46 +00:00
|
|
|
// e = video object
|
2020-08-07 23:37:27 +00:00
|
|
|
var frameSelector = {
|
|
|
|
ke: e.ke,
|
|
|
|
mid: e.id,
|
2022-12-21 19:26:45 +00:00
|
|
|
type: cloudType,
|
2020-08-07 23:37:27 +00:00
|
|
|
time: new Date(e.time),
|
|
|
|
}
|
2020-07-11 00:30:08 +00:00
|
|
|
s.knexQuery({
|
|
|
|
action: "select",
|
|
|
|
columns: "*",
|
|
|
|
table: "Cloud Timelapse Frames",
|
2020-08-06 01:15:10 +00:00
|
|
|
where: frameSelector,
|
|
|
|
limit: 1
|
2020-07-11 00:30:08 +00:00
|
|
|
},function(err,r){
|
|
|
|
if(r && r[0]){
|
2019-07-08 03:06:46 +00:00
|
|
|
r = r[0]
|
2020-07-11 00:30:08 +00:00
|
|
|
s.knexQuery({
|
|
|
|
action: "delete",
|
|
|
|
table: "Cloud Timelapse Frames",
|
2020-08-06 01:15:10 +00:00
|
|
|
where: frameSelector,
|
|
|
|
limit: 1
|
2020-07-11 00:30:08 +00:00
|
|
|
},function(){
|
2022-12-21 19:26:45 +00:00
|
|
|
s.onDeleteTimelapseFrameFromCloudExtensionsRunner(e,details.type || r.type || 's3',r)
|
2019-07-08 03:06:46 +00:00
|
|
|
})
|
|
|
|
}else{
|
|
|
|
// console.log('Delete Failed',e)
|
2020-10-10 05:49:11 +00:00
|
|
|
// console.error(err)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
const deleteTimelapseFrame = function(e){
|
|
|
|
// e = video object
|
|
|
|
s.checkDetails(e)
|
|
|
|
var frameSelector = {
|
|
|
|
ke: e.ke,
|
|
|
|
mid: e.mid,
|
|
|
|
filename: e.filename,
|
|
|
|
}
|
|
|
|
s.knexQuery({
|
|
|
|
action: "select",
|
|
|
|
columns: "*",
|
|
|
|
table: "Timelapse Frames",
|
|
|
|
where: frameSelector,
|
|
|
|
limit: 1
|
|
|
|
},function(err,r){
|
|
|
|
if(r && r[0]){
|
|
|
|
r = r[0]
|
|
|
|
s.knexQuery({
|
|
|
|
action: "delete",
|
|
|
|
table: "Timelapse Frames",
|
|
|
|
where: frameSelector,
|
|
|
|
limit: 1
|
2022-06-27 20:21:51 +00:00
|
|
|
},async function(){
|
2023-01-21 15:41:03 +00:00
|
|
|
s.setDiskUsedForGroup(e.ke,-(r.size / 1048576),'timelapseFrames')
|
2020-10-10 05:49:11 +00:00
|
|
|
s.file('delete',e.fileLocation)
|
2022-06-27 20:21:51 +00:00
|
|
|
const fileDirectory = getFileDirectory(folderPath);
|
|
|
|
const folderIsEmpty = (await fs.promises.readdir(folderPath)).filter(file => file.indexOf('.jpg') > -1).length === 0;
|
|
|
|
if(folderIsEmpty){
|
2022-12-27 23:30:59 +00:00
|
|
|
await fs.rm(folderPath, { recursive: true })
|
2022-06-27 20:21:51 +00:00
|
|
|
}
|
2020-10-10 05:49:11 +00:00
|
|
|
})
|
|
|
|
}else{
|
|
|
|
// console.log('Delete Failed',e)
|
2019-07-08 03:06:46 +00:00
|
|
|
// console.error(err)
|
|
|
|
}
|
2019-06-07 16:52:07 +00:00
|
|
|
})
|
|
|
|
}
|
2022-06-27 07:30:22 +00:00
|
|
|
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)
|
2023-03-28 02:08:07 +00:00
|
|
|
const videoBuildProcess = spawn(config.ffmpegDir,splitForFFMPEG(commandString))
|
2022-06-27 07:30:22 +00:00
|
|
|
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 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,
|
|
|
|
})
|
2022-07-07 22:53:46 +00:00
|
|
|
await fs.promises.rm(concatListFile)
|
|
|
|
for (let i = 0; i < filePathsList.length; i++) {
|
2022-06-27 07:30:22 +00:00
|
|
|
var segmentFileOutput = filePathsList[i]
|
2022-07-07 22:53:46 +00:00
|
|
|
await fs.promises.rm(segmentFileOutput)
|
2022-06-27 07:30:22 +00:00
|
|
|
}
|
|
|
|
s.debugLog('videoBuildProcess Stitching Complete!',finalMp4OutputLocation)
|
|
|
|
}
|
|
|
|
async function createVideoFromTimelapse(timelapseFrames,framesPerSecond){
|
2022-06-26 07:56:15 +00:00
|
|
|
s.debugLog("Building Timelapse Frames Video",timelapseFrames.length)
|
|
|
|
framesPerSecond = !isNaN(framesPerSecond) ? framesPerSecond : parseInt(framesPerSecond) || 2
|
|
|
|
const frames = timelapseFrames.reverse()
|
|
|
|
const numberOfFrames = timelapseFrames.length
|
|
|
|
const ke = frames[0].ke
|
|
|
|
const mid = frames[0].mid
|
|
|
|
const activeMonitor = s.group[ke].activeMonitors[mid]
|
|
|
|
const finalFileName = `${s.md5(JSON.stringify(frames))}-${framesPerSecond}fps.mp4`
|
|
|
|
const finalMp4OutputLocation = `${s.dir.fileBin}${ke}/${mid}/${finalFileName}`
|
|
|
|
const finalFileAlreadyExist = fs.existsSync(finalMp4OutputLocation)
|
|
|
|
const concatListFile = `${s.dir.streams}${ke}/${mid}/mergeJpegs_${finalFileName}.txt`
|
|
|
|
const response = {
|
|
|
|
ok: false,
|
|
|
|
ke: ke,
|
|
|
|
mid: mid,
|
|
|
|
name: finalFileName,
|
|
|
|
}
|
|
|
|
s.debugLog("activeMonitor.buildingTimelapseVideo",!!activeMonitor.buildingTimelapseVideo)
|
|
|
|
if(activeMonitor.buildingTimelapseVideo){
|
|
|
|
s.debugLog("Timelapse Frames Video Building Already",finalMp4OutputLocation)
|
|
|
|
return activeMonitor.buildingTimelapseVideo
|
|
|
|
}
|
|
|
|
s.debugLog("finalFileAlreadyExist",finalFileAlreadyExist)
|
|
|
|
if(finalFileAlreadyExist){
|
|
|
|
s.debugLog("Timelapse Frames Video finalFileAlreadyExist",finalMp4OutputLocation)
|
|
|
|
response.fileExists = true
|
|
|
|
response.msg = lang['Already exists']
|
|
|
|
return response
|
|
|
|
}
|
2022-06-27 07:30:22 +00:00
|
|
|
if(frames.length < framesPerSecond){
|
2022-06-26 07:56:15 +00:00
|
|
|
response.msg = lang.notEnoughFramesText1
|
|
|
|
return response
|
|
|
|
}
|
|
|
|
activeMonitor.buildingTimelapseVideo = response
|
2022-06-27 07:30:22 +00:00
|
|
|
chunkFramesAndBuildMultipleVideosThenSticth({
|
|
|
|
frames: frames,
|
|
|
|
listFile: concatListFile,
|
|
|
|
fps: framesPerSecond,
|
|
|
|
output: finalMp4OutputLocation,
|
2022-06-27 07:41:57 +00:00
|
|
|
finalFileName: finalFileName
|
2022-06-27 07:30:22 +00:00
|
|
|
}).then(async () => {
|
|
|
|
// videoBuildProcess exit
|
2022-06-26 07:56:15 +00:00
|
|
|
s.debugLog('videoBuildProcess exit',finalMp4OutputLocation)
|
|
|
|
const timeNow = new Date()
|
2022-06-27 07:30:22 +00:00
|
|
|
const fileStats = await fs.promises.stat(finalMp4OutputLocation)
|
|
|
|
const details = {
|
|
|
|
start: frames[0].time,
|
|
|
|
end: frames[frames.length - 1].time,
|
|
|
|
}
|
|
|
|
s.knexQuery({
|
|
|
|
action: "insert",
|
|
|
|
table: "Files",
|
|
|
|
insert: {
|
2022-06-26 07:56:15 +00:00
|
|
|
ke: ke,
|
|
|
|
mid: mid,
|
2022-06-27 07:30:22 +00:00
|
|
|
details: s.s(details),
|
2022-06-26 07:56:15 +00:00
|
|
|
name: finalFileName,
|
|
|
|
size: fileStats.size,
|
|
|
|
time: timeNow,
|
2022-06-27 07:30:22 +00:00
|
|
|
}
|
|
|
|
})
|
|
|
|
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)
|
2022-06-26 07:56:15 +00:00
|
|
|
})
|
|
|
|
response.ok = true
|
2022-06-27 22:03:55 +00:00
|
|
|
response.msg = `${lang.Building}... ${lang['Please Wait...']}`
|
2022-06-26 07:56:15 +00:00
|
|
|
return response
|
|
|
|
}
|
2022-07-05 03:29:26 +00:00
|
|
|
function initiateTimelapseVideoBuild({
|
|
|
|
groupKey,
|
|
|
|
monitorId,
|
|
|
|
framesPerSecond,
|
|
|
|
framesPosted,
|
|
|
|
}){
|
|
|
|
return new Promise((resolve,reject) => {
|
|
|
|
let response = {ok: false}
|
2022-07-07 19:33:19 +00:00
|
|
|
if(!monitorId){
|
|
|
|
response.msg = lang['No Monitor Found, Ignoring Request']
|
2022-07-05 03:29:26 +00:00
|
|
|
resolve(response)
|
2022-07-07 19:33:19 +00:00
|
|
|
}else{
|
|
|
|
const frames = []
|
|
|
|
var n = 0
|
|
|
|
framesPosted.forEach((frame) => {
|
|
|
|
var firstParam = [['ke','=',groupKey],['mid','=',monitorId],['filename','=',frame.filename]]
|
|
|
|
if(n !== 0)firstParam[0] = (['or']).concat(firstParam[0])
|
|
|
|
frames.push(...firstParam)
|
|
|
|
++n
|
|
|
|
})
|
|
|
|
s.knexQuery({
|
|
|
|
action: "select",
|
|
|
|
columns: "*",
|
|
|
|
table: "Timelapse Frames",
|
|
|
|
where: frames
|
|
|
|
},async (err,r) => {
|
|
|
|
if(r.length > 0){
|
|
|
|
response = await createVideoFromTimelapse(r.reverse(),framesPerSecond)
|
|
|
|
}
|
|
|
|
resolve(response)
|
|
|
|
})
|
|
|
|
}
|
2022-07-05 03:29:26 +00:00
|
|
|
})
|
|
|
|
}
|
2019-07-10 06:12:17 +00:00
|
|
|
// Web Paths
|
|
|
|
// // // // //
|
|
|
|
/**
|
|
|
|
* API : Get Timelapse images
|
|
|
|
*/
|
|
|
|
app.get([
|
|
|
|
config.webPaths.apiPrefix+':auth/timelapse/:ke',
|
|
|
|
config.webPaths.apiPrefix+':auth/timelapse/:ke/:id',
|
|
|
|
config.webPaths.apiPrefix+':auth/timelapse/:ke/:id/:date',
|
2022-08-08 22:16:59 +00:00
|
|
|
config.webPaths.apiPrefix+':auth/cloudTimelapse/:ke',
|
|
|
|
config.webPaths.apiPrefix+':auth/cloudTimelapse/:ke/:id',
|
|
|
|
config.webPaths.apiPrefix+':auth/cloudTimelapse/:ke/:id/:date',
|
2019-07-10 06:12:17 +00:00
|
|
|
], function (req,res){
|
|
|
|
res.setHeader('Content-Type', 'application/json');
|
|
|
|
s.auth(req.params,function(user){
|
2022-08-06 06:20:08 +00:00
|
|
|
const monitorId = req.params.id
|
|
|
|
const groupKey = req.params.ke
|
|
|
|
const {
|
|
|
|
monitorPermissions,
|
|
|
|
monitorRestrictions,
|
|
|
|
} = s.getMonitorsPermitted(user.details,monitorId)
|
|
|
|
const {
|
|
|
|
isRestricted,
|
|
|
|
isRestrictedApiKey,
|
|
|
|
apiKeyPermissions,
|
|
|
|
} = s.checkPermission(user);
|
2019-07-10 06:12:17 +00:00
|
|
|
if(
|
2022-08-06 06:20:08 +00:00
|
|
|
isRestrictedApiKey && apiKeyPermissions.watch_videos_disallowed ||
|
|
|
|
isRestricted && (
|
|
|
|
monitorId && !monitorPermissions[`${monitorId}_video_view`] ||
|
|
|
|
monitorRestrictions.length === 0
|
2020-07-11 00:30:08 +00:00
|
|
|
)
|
2019-07-10 06:12:17 +00:00
|
|
|
){
|
2022-08-06 06:20:08 +00:00
|
|
|
s.closeJsonResponse(res,{ok: false, msg: lang['Not Authorized'], frames: []});
|
2019-07-10 06:12:17 +00:00
|
|
|
return
|
|
|
|
}
|
2022-08-08 22:16:59 +00:00
|
|
|
var origURL = req.originalUrl.split('/')
|
|
|
|
var videoParam = origURL[origURL.indexOf(req.params.auth) + 1]
|
|
|
|
var dataSet = 'Timelapse Frames'
|
|
|
|
switch(videoParam){
|
|
|
|
case'cloudTimelapse':
|
|
|
|
dataSet = 'Cloud Timelapse Frames'
|
|
|
|
break;
|
|
|
|
}
|
2020-07-11 21:11:39 +00:00
|
|
|
s.getDatabaseRows({
|
2020-07-11 00:30:08 +00:00
|
|
|
monitorRestrictions: monitorRestrictions,
|
2022-08-08 22:16:59 +00:00
|
|
|
table: dataSet,
|
2020-07-11 00:30:08 +00:00
|
|
|
groupKey: req.params.ke,
|
|
|
|
date: req.query.date,
|
2020-07-11 21:11:39 +00:00
|
|
|
startDate: req.query.start,
|
|
|
|
endDate: req.query.end,
|
2020-07-11 00:30:08 +00:00
|
|
|
startOperator: req.query.startOperator,
|
|
|
|
endOperator: req.query.endOperator,
|
2021-12-03 18:20:56 +00:00
|
|
|
noLimit: req.query.noLimit,
|
2020-07-11 00:30:08 +00:00
|
|
|
limit: req.query.limit,
|
|
|
|
archived: req.query.archived,
|
2020-07-28 01:12:57 +00:00
|
|
|
rowType: 'frames',
|
|
|
|
endIsStartTo: true
|
2020-07-11 00:30:08 +00:00
|
|
|
},(response) => {
|
2022-06-27 07:30:22 +00:00
|
|
|
s.closeJsonResponse(res,response.frames)
|
2019-07-10 06:12:17 +00:00
|
|
|
})
|
|
|
|
},res,req);
|
|
|
|
});
|
|
|
|
/**
|
2020-10-09 19:34:15 +00:00
|
|
|
* API : Build MP4 File
|
|
|
|
*/
|
|
|
|
app.post([
|
|
|
|
config.webPaths.apiPrefix+':auth/timelapseBuildVideo/:ke',
|
|
|
|
config.webPaths.apiPrefix+':auth/timelapseBuildVideo/:ke/:id',
|
|
|
|
], function (req,res){
|
|
|
|
res.setHeader('Content-Type', 'application/json');
|
|
|
|
s.auth(req.params,function(user){
|
2022-07-05 03:29:26 +00:00
|
|
|
const groupKey = req.params.ke
|
|
|
|
const monitorId = req.params.id
|
2022-08-06 06:20:08 +00:00
|
|
|
const actionParameter = !!req.params.action
|
|
|
|
const {
|
|
|
|
monitorPermissions,
|
|
|
|
monitorRestrictions,
|
|
|
|
} = s.getMonitorsPermitted(user.details,monitorId)
|
|
|
|
const {
|
|
|
|
isRestricted,
|
|
|
|
isRestrictedApiKey,
|
|
|
|
apiKeyPermissions,
|
|
|
|
} = s.checkPermission(user)
|
2020-10-09 19:34:15 +00:00
|
|
|
if(
|
2022-08-06 06:20:08 +00:00
|
|
|
isRestrictedApiKey && apiKeyPermissions.delete_videos_disallowed ||
|
|
|
|
isRestricted && !monitorPermissions[`${monitorId}_video_delete`]
|
2020-10-09 19:34:15 +00:00
|
|
|
){
|
2022-08-06 06:20:08 +00:00
|
|
|
s.closeJsonResponse(res,{ok: false, msg: lang['Not Authorized']});
|
2020-10-09 19:34:15 +00:00
|
|
|
return
|
|
|
|
}
|
2022-07-05 03:29:26 +00:00
|
|
|
const framesPerSecond = s.getPostData(req, 'fps')
|
2020-10-09 19:34:15 +00:00
|
|
|
const framesPosted = s.getPostData(req, 'frames', true) || []
|
2022-07-05 03:29:26 +00:00
|
|
|
initiateTimelapseVideoBuild({
|
|
|
|
groupKey,
|
|
|
|
monitorId,
|
|
|
|
framesPosted,
|
|
|
|
framesPerSecond,
|
|
|
|
}).then((buildResponse) => {
|
2022-06-27 07:41:57 +00:00
|
|
|
s.closeJsonResponse(res,buildResponse)
|
2020-10-09 19:34:15 +00:00
|
|
|
})
|
|
|
|
},res,req);
|
|
|
|
});
|
|
|
|
/**
|
2019-07-10 06:12:17 +00:00
|
|
|
* API : Get Timelapse images
|
|
|
|
*/
|
|
|
|
app.get([
|
|
|
|
config.webPaths.apiPrefix+':auth/timelapse/:ke/:id/:date/:filename',
|
2020-10-10 05:49:11 +00:00
|
|
|
config.webPaths.apiPrefix+':auth/timelapse/:ke/:id/:date/:filename/:action',
|
2019-07-10 06:12:17 +00:00
|
|
|
], function (req,res){
|
|
|
|
res.setHeader('Content-Type', 'application/json');
|
|
|
|
s.auth(req.params,function(user){
|
2022-08-06 06:20:08 +00:00
|
|
|
const groupKey = req.params.ke
|
|
|
|
const monitorId = req.params.id
|
|
|
|
const actionParameter = !!req.params.action
|
|
|
|
const {
|
|
|
|
monitorPermissions,
|
|
|
|
monitorRestrictions,
|
|
|
|
} = s.getMonitorsPermitted(user.details,monitorId)
|
|
|
|
const {
|
|
|
|
isRestricted,
|
|
|
|
isRestrictedApiKey,
|
|
|
|
apiKeyPermissions,
|
|
|
|
} = s.checkPermission(user)
|
2019-07-10 06:12:17 +00:00
|
|
|
if(
|
2022-08-06 06:20:08 +00:00
|
|
|
actionParameter && (
|
|
|
|
isRestrictedApiKey && apiKeyPermissions.delete_videos_disallowed ||
|
|
|
|
isRestricted && !monitorPermissions[`${monitorId}_video_delete`]
|
|
|
|
) ||
|
|
|
|
!actionParameter && (
|
|
|
|
isRestrictedApiKey && apiKeyPermissions.watch_videos_disallowed ||
|
|
|
|
isRestricted && monitorId && !monitorPermissions[`${monitorId}_video_view`]
|
|
|
|
)
|
2019-07-10 06:12:17 +00:00
|
|
|
){
|
2022-08-06 06:20:08 +00:00
|
|
|
s.closeJsonResponse(res,{ok: false, msg: lang['Not Authorized']});
|
2019-07-10 06:12:17 +00:00
|
|
|
return
|
|
|
|
}
|
2020-10-10 05:49:11 +00:00
|
|
|
const cacheKey = req.params.ke + req.params.id + req.params.filename
|
|
|
|
const processFrame = (frame) => {
|
|
|
|
var fileLocation
|
|
|
|
if(frame.details.dir){
|
|
|
|
fileLocation = `${s.checkCorrectPathEnding(frame.details.dir)}`
|
|
|
|
}else{
|
|
|
|
fileLocation = `${s.dir.videos}`
|
|
|
|
}
|
|
|
|
var selectedDate = req.params.date
|
|
|
|
if(selectedDate.indexOf('-') === -1){
|
|
|
|
selectedDate = req.params.filename.split('T')[0]
|
|
|
|
}
|
|
|
|
fileLocation = `${fileLocation}${frame.ke}/${frame.mid}_timelapse/${selectedDate}/${req.params.filename}`
|
2022-08-06 06:20:08 +00:00
|
|
|
if(actionParameter === 'delete'){
|
2021-02-11 17:45:05 +00:00
|
|
|
deleteTimelapseFrame({
|
|
|
|
ke: frame.ke,
|
|
|
|
mid: frame.mid,
|
|
|
|
filename: req.params.filename,
|
|
|
|
fileLocation: fileLocation,
|
|
|
|
})
|
|
|
|
delete(timelapseFramesCache[cacheKey])
|
|
|
|
s.closeJsonResponse(res,{ok: true})
|
|
|
|
}else{
|
|
|
|
fs.stat(fileLocation,function(err,stats){
|
|
|
|
if(!err){
|
2019-09-08 06:17:57 +00:00
|
|
|
res.contentType('image/jpeg')
|
|
|
|
res.on('finish',function(){res.end()})
|
|
|
|
fs.createReadStream(fileLocation).pipe(res)
|
2021-02-11 17:45:05 +00:00
|
|
|
}else{
|
|
|
|
s.closeJsonResponse(res,{ok: false, msg: lang[`Nothing exists`]})
|
2019-09-08 06:17:57 +00:00
|
|
|
}
|
2021-05-20 03:18:19 +00:00
|
|
|
})
|
2021-02-11 17:45:05 +00:00
|
|
|
}
|
2020-10-10 05:49:11 +00:00
|
|
|
}
|
|
|
|
if(timelapseFramesCache[cacheKey]){
|
|
|
|
processFrame(timelapseFramesCache[cacheKey])
|
|
|
|
}else{
|
|
|
|
s.getDatabaseRows({
|
|
|
|
monitorRestrictions: monitorRestrictions,
|
|
|
|
table: 'Timelapse Frames',
|
|
|
|
groupKey: req.params.ke,
|
|
|
|
archived: req.query.archived,
|
|
|
|
filename: req.params.filename,
|
2021-12-07 06:35:19 +00:00
|
|
|
limit: 1,
|
2020-10-10 05:49:11 +00:00
|
|
|
rowType: 'frames',
|
|
|
|
endIsStartTo: true
|
|
|
|
},(response) => {
|
|
|
|
var frame = response.frames[0]
|
|
|
|
if(frame){
|
|
|
|
timelapseFramesCache[cacheKey] = frame
|
|
|
|
timelapseFramesCacheTimeouts[cacheKey] = setTimeout(function(){
|
|
|
|
delete(timelapseFramesCache[cacheKey])
|
|
|
|
},1000 * 60 * 10)
|
|
|
|
processFrame(frame)
|
|
|
|
}else{
|
|
|
|
s.closeJsonResponse(res,{ok: false, msg: lang[`Nothing exists`]})
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
2019-07-10 06:12:17 +00:00
|
|
|
},res,req);
|
|
|
|
});
|
|
|
|
/**
|
|
|
|
* Page : Get Timelapse Page (Not Modal)
|
|
|
|
*/
|
|
|
|
app.get(config.webPaths.apiPrefix+':auth/timelapsePage/:ke', function (req,res){
|
|
|
|
req.params.protocol=req.protocol;
|
|
|
|
s.auth(req.params,function(user){
|
|
|
|
// if(user.permissions.watch_stream==="0"||user.details.sub&&user.details.allmonitors!=='1'&&user.details.monitors.indexOf(req.params.id)===-1){
|
|
|
|
// res.end(user.lang['Not Permitted'])
|
|
|
|
// return
|
|
|
|
// }
|
|
|
|
req.params.uid = user.uid
|
|
|
|
s.renderPage(req,res,config.renderPaths.timelapse,{
|
|
|
|
$user: user,
|
|
|
|
data: req.params,
|
|
|
|
config: s.getConfigWithBranding(req.hostname),
|
|
|
|
lang: user.lang,
|
|
|
|
originalURL: s.getOriginalUrl(req)
|
|
|
|
})
|
|
|
|
},res,req);
|
|
|
|
});
|
2022-07-05 03:29:26 +00:00
|
|
|
s.onOtherWebSocketMessages((d,connection) => {
|
|
|
|
switch(d.f){
|
|
|
|
case'timelapseVideoBuild':
|
|
|
|
initiateTimelapseVideoBuild({
|
|
|
|
groupKey: d.ke,
|
|
|
|
monitorId: d.mid,
|
|
|
|
framesPosted: d.frames,
|
|
|
|
framesPerSecond: d.fps,
|
|
|
|
}).then((buildResponse) => {
|
|
|
|
s.tx({
|
|
|
|
f: 'timelapse_build_requested',
|
|
|
|
ke: d.ke,
|
|
|
|
mid: d.mid,
|
|
|
|
buildResponse: buildResponse,
|
|
|
|
},'GRP_'+d.ke);
|
|
|
|
})
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
})
|
2022-07-06 02:25:30 +00:00
|
|
|
function buildTimelapseVideos(){
|
|
|
|
return new Promise((resolve,reject) => {
|
|
|
|
var dateNow = new Date()
|
|
|
|
var hoursNow = dateNow.getHours()
|
|
|
|
if(hoursNow === 1){
|
|
|
|
var dateNowMoment = moment(dateNow).utc().format('YYYY-MM-DDTHH:mm:ss')
|
|
|
|
var dateMinusOneDay = moment(dateNow).utc().subtract(1, 'days').format('YYYY-MM-DDTHH:mm:ss')
|
|
|
|
s.knexQuery({
|
|
|
|
action: "select",
|
|
|
|
columns: "*",
|
|
|
|
table: "Timelapse Frames",
|
|
|
|
where: [
|
|
|
|
['time','=>',dateMinusOneDay],
|
|
|
|
['time','=<',dateNowMoment],
|
|
|
|
]
|
|
|
|
},async function(err,frames) {
|
|
|
|
var groups = {}
|
|
|
|
frames.forEach(function(frame){
|
|
|
|
if(groups[frame.ke])groups[frame.ke] = {}
|
|
|
|
if(groups[frame.ke][frame.mid])groups[frame.ke][frame.mid] = []
|
|
|
|
groups[frame.ke][frame.mid].push(frame)
|
2019-07-10 06:12:17 +00:00
|
|
|
})
|
2022-07-06 02:25:30 +00:00
|
|
|
const groupKeys = Object.keys(groups);
|
|
|
|
for (let i = 0; i < groupKeys.length; i++) {
|
|
|
|
const groupKey = groupKeys[i]
|
|
|
|
const monitorIds = Object.keys(groups[groupKey]);
|
|
|
|
for (let ii = 0; ii < monitorIds.length; ii++) {
|
|
|
|
const monitorId = monitorIds[ii]
|
|
|
|
const frameSet = groups[groupKey][monitorId]
|
|
|
|
await createVideoFromTimelapse(frameSet,30)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
resolve()
|
2019-07-10 06:12:17 +00:00
|
|
|
})
|
2022-07-06 02:25:30 +00:00
|
|
|
}else{
|
|
|
|
resolve()
|
|
|
|
}
|
|
|
|
})
|
2019-07-30 14:31:34 +00:00
|
|
|
}
|
|
|
|
// Auto Build Timelapse Videos
|
|
|
|
if(config.autoBuildTimelapseVideosDaily === true){
|
|
|
|
setInterval(buildTimelapseVideos,1000 * 60 * 60 * 0.75)//every 45 minutes
|
|
|
|
buildTimelapseVideos()
|
2019-07-10 06:12:17 +00:00
|
|
|
}
|
2019-06-07 16:52:07 +00:00
|
|
|
}
|