Shinobi/libs/videos.js

625 lines
26 KiB
JavaScript

var fs = require('fs');
var exec = require('child_process').exec;
var spawn = require('child_process').spawn;
module.exports = function(s,config,lang){
/**
* Gets the video directory of the supplied video or monitor database row.
* @constructor
* @param {object} e - Monitor object or Video object. Object is a database row.
*/
s.getVideoDirectory = 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+'/'
}else{
return s.dir.videos+e.ke+'/'+e.id+'/';
}
}
/**
* Creates available API based URLs for streaming
* @constructor
* @param {object} videos - Array of video objects
* @param {object} options - Contains middle parameter of URL and auth key
* @param [options.auth] {string} - API Key
* @param [options.videoParam] {string} - currently only `videos` and `cloudVideos` available.
*/
s.buildVideoLinks = function(videos,options){
videos.forEach(function(v){
var details = s.parseJSON(v.details)
var queryString = []
if(details.isUTC === true){
queryString.push('isUTC=true')
}else{
v.time = s.utcToLocal(v.time)
v.end = s.utcToLocal(v.end)
}
if(queryString.length > 0){
queryString = '?'+queryString.join('&')
}else{
queryString = ''
}
if(details.type === 'googd'){
v.ext = v.ext ? v.ext : 'mp4'
v.href = undefined
}else if(!v.ext && v.href){
v.ext = v.href.split('.')
v.ext = v.ext[v.ext.length - 1]
}
v.filename = s.formattedTime(v.time)+'.'+v.ext;
if(!options.videoParam)options.videoParam = 'videos'
var href = s.checkCorrectPathEnding(config.webPaths.apiPrefix) + options.auth+'/'+options.videoParam+'/'+v.ke+'/'+v.mid+'/'+v.filename;
v.actionUrl = href
v.links = {
deleteVideo : href+'/delete' + queryString,
changeToUnread : href+'/status/1' + queryString,
changeToRead : href+'/status/2' + queryString
}
if(!v.href || options.hideRemote === true)v.href = href + queryString
v.details = details
})
}
s.insertDatabaseRow = function(e,k,callback){
s.checkDetails(e)
//save database row
if(!k.details)k.details = {}
if(e.details && e.details.dir && e.details.dir !== ''){
k.details.dir = e.details.dir
}
if(config.useUTC === true)k.details.isUTC = config.useUTC;
s.knexQuery({
action: "insert",
table: "Videos",
insert: {
ke: e.ke,
mid: e.mid,
time: k.startTime,
ext: e.ext,
status: 1,
details: s.s(k.details),
size: k.filesize,
end: k.endTime,
}
},(err) => {
if(callback)callback(err)
fs.chmod(k.dir+k.file,0o777,function(err){
})
})
}
//on video completion
s.insertCompletedVideo = function(e,k,callback){
//e = monitor object
//k = video insertion object
s.checkDetails(e)
if(!k)k={};
e.dir = s.getVideoDirectory(e)
k.dir = e.dir.toString()
if(s.group[e.ke].activeMonitors[e.id].childNode){
s.cx({
f: 'insertCompleted',
d: s.group[e.ke].rawMonitorConfigurations[e.id],
k: k
},s.group[e.ke].activeMonitors[e.id].childNodeId);
}else{
//get file directory
k.fileExists = fs.existsSync(k.dir+k.file)
if(k.fileExists!==true){
k.dir = s.dir.videos+'/'+e.ke+'/'+e.id+'/'
k.fileExists = fs.existsSync(k.dir+k.file)
if(k.fileExists !== true){
s.dir.addStorage.forEach(function(v){
if(k.fileExists !== true){
k.dir = s.checkCorrectPathEnding(v.path)+e.ke+'/'+e.id+'/'
k.fileExists = fs.existsSync(k.dir+k.file)
}
})
}
}
if(k.fileExists===true){
//close video row
k.details = k.details && k.details instanceof Object ? k.details : {}
k.stat = fs.statSync(k.dir+k.file)
k.filesize = k.stat.size
k.filesizeMB = parseFloat((k.filesize/1048576).toFixed(2))
k.startTime = new Date(s.nameToTime(k.file))
k.endTime = new Date(k.endTime || k.stat.mtime)
if(config.useUTC === true){
fs.rename(k.dir+k.file, k.dir+s.formattedTime(k.startTime)+'.'+e.ext, (err) => {
if (err) return console.error(err);
});
k.filename = s.formattedTime(k.startTime)+'.'+e.ext
}else{
k.filename = k.file
}
if(!e.ext){e.ext = k.filename.split('.')[1]}
//send event for completed recording
const response = {
mid: e.mid,
ke: e.ke,
filename: k.filename,
d: s.cleanMonitorObject(s.group[e.ke].rawMonitorConfigurations[e.id]),
filesize: k.filesize,
time: s.timeObject(k.startTime).format('YYYY-MM-DD HH:mm:ss'),
end: s.timeObject(k.endTime).format('YYYY-MM-DD HH:mm:ss')
}
if(config.childNodes.enabled === true && config.childNodes.mode === 'child' && config.childNodes.host){
fs.createReadStream(k.dir+k.filename,{ highWaterMark: 500 })
.on('data',function(data){
s.cx(Object.assign(response,{
f:'created_file_chunk',
chunk: data,
}))
})
.on('close',function(){
clearTimeout(s.group[e.ke].activeMonitors[e.id].recordingChecker)
clearTimeout(s.group[e.ke].activeMonitors[e.id].streamChecker)
s.cx(Object.assign(response,{
f:'created_file',
}))
})
}else{
var href = '/videos/'+e.ke+'/'+e.mid+'/'+k.filename
if(config.useUTC === true)href += '?isUTC=true';
const monitorEventsCounted = s.group[e.ke].activeMonitors[e.mid].detector_motion_count
s.txWithSubPermissions({
f: 'video_build_success',
hrefNoAuth: href,
filename: k.filename,
mid: e.mid,
ke: e.ke,
time: k.startTime,
size: k.filesize,
end: k.endTime,
events: monitorEventsCounted && monitorEventsCounted.length > 0 ? monitorEventsCounted : null
},'GRP_'+e.ke,'video_view')
//purge over max
s.purgeDiskForGroup(e.ke)
//send new diskUsage values
var storageIndex = s.getVideoStorageIndex(e)
if(storageIndex){
s.setDiskUsedForGroupAddStorage(e.ke,{
size: k.filesizeMB,
storageIndex: storageIndex
})
}else{
s.setDiskUsedForGroup(e.ke,k.filesizeMB)
}
s.onBeforeInsertCompletedVideoExtensions.forEach(function(extender){
extender(e,k)
})
s.insertDatabaseRow(e,k,callback)
s.insertCompletedVideoExtensions.forEach(function(extender){
extender(e,k,response)
})
}
}
}
s.group[e.ke].activeMonitors[e.mid].detector_motion_count = []
}
s.deleteVideo = function(e){
//e = video object
s.checkDetails(e)
e.dir = s.getVideoDirectory(e)
if(!e.filename && e.time){
e.filename = s.formattedTime(e.time)
}
var filename,
time
if(e.filename.indexOf('.')>-1){
filename = e.filename
}else{
filename = e.filename+'.'+e.ext
}
if(e.filename && !e.time){
time = s.nameToTime(filename)
}else{
time = e.time
}
time = new Date(time)
const whereQuery = {
ke: e.ke,
mid: e.id,
time: time,
}
s.knexQuery({
action: "select",
columns: "*",
table: "Videos",
where: whereQuery
},(err,r) => {
if(r && r[0]){
r = r[0]
fs.chmod(e.dir+filename,0o777,function(err){
s.tx({
f: 'video_delete',
filename: filename,
mid: e.id,
ke: e.ke,
time: s.nameToTime(filename),
end: s.formattedTime(new Date,'YYYY-MM-DD HH:mm:ss')
},'GRP_'+e.ke);
var storageIndex = s.getVideoStorageIndex(e)
if(storageIndex){
s.setDiskUsedForGroupAddStorage(e.ke,{
size: -(r.size / 1048576),
storageIndex: storageIndex
})
}else{
s.setDiskUsedForGroup(e.ke,-(r.size / 1048576))
}
s.knexQuery({
action: "delete",
table: "Videos",
where: whereQuery
},(err) => {
if(err){
s.systemLog(lang['File Delete Error'] + ' : '+e.ke+' : '+' : '+e.id,err)
}
})
fs.unlink(e.dir+filename,function(err){
fs.stat(e.dir+filename,function(err){
if(!err){
s.file('delete',e.dir+filename)
}
})
})
})
}else{
console.log(lang['Database row does not exist'],whereQuery)
}
})
}
s.deleteListOfVideos = function(videos){
var deleteSetOfVideos = function(videos){
const whereQuery = []
var didOne = false;
videos.forEach(function(video){
s.checkDetails(video)
//e = video object
video.dir = s.getVideoDirectory(video)
if(!video.filename && video.time){
video.filename = s.formattedTime(video.time)
}
var filename,
time
if(video.filename.indexOf('.')>-1){
filename = video.filename
}else{
filename = video.filename+'.'+video.ext
}
if(video.filename && !video.time){
time = s.nameToTime(filename)
}else{
time = video.time
}
time = new Date(time)
fs.chmod(video.dir + filename,0o777,function(err){
s.tx({
f: 'video_delete',
filename: filename,
mid: video.mid,
ke: video.ke,
time: s.nameToTime(filename),
end: s.formattedTime(new Date,'YYYY-MM-DD HH:mm:ss')
},'GRP_'+video.ke);
var storageIndex = s.getVideoStorageIndex(video)
if(storageIndex){
s.setDiskUsedForGroupAddStorage(video.ke,{
size: -(video.size / 1048576),
storageIndex: storageIndex
})
}else{
s.setDiskUsedForGroup(video.ke,-(video.size / 1048576))
}
fs.unlink(video.dir + filename,function(err){
fs.stat(video.dir + filename,function(err){
if(!err){
s.file('delete',video.dir + filename)
}
})
})
})
const queryGroup = {
ke: video.ke,
mid: video.mid,
time: time,
}
if(whereQuery.length > 0)queryGroup.__separator = 'or'
whereQuery.push(queryGroup)
})
s.knexQuery({
action: "delete",
table: "Videos",
where: whereQuery
},(err) => {
if(err){
s.systemLog(lang['List of Videos Delete Error'],err)
}
})
}
videos.chunk(100).forEach(function(videosChunk){
deleteSetOfVideos(videosChunk)
})
}
s.deleteVideoFromCloudExtensions = {}
s.deleteVideoFromCloudExtensionsRunner = function(e,storageType,video){
// e = user
if(!storageType){
var videoDetails = JSON.parse(video.details)
videoDetails.type = videoDetails.type || 's3'
}
if(s.deleteVideoFromCloudExtensions[storageType]){
s.deleteVideoFromCloudExtensions[storageType](e,video,function(){
s.tx({
f: 'video_delete_cloud',
mid: e.mid,
ke: e.ke,
time: e.time,
end: e.end
},'GRP_'+e.ke);
})
}
}
s.deleteVideoFromCloud = function(e){
// e = video object
s.checkDetails(e)
const whereQuery = {
ke: e.ke,
mid: e.mid,
time: new Date(e.time),
}
s.knexQuery({
action: "select",
columns: "*",
table: "Cloud Videos",
where: whereQuery
},(err,r) => {
if(r&&r[0]){
r = r[0]
var details = s.parseJSON(r.details) || {}
s.knexQuery({
action: "delete",
table: "Cloud Videos",
where: whereQuery
},(err) => {
s.deleteVideoFromCloudExtensionsRunner(e,details.type || 's3',r)
})
}else{
// console.log('Delete Failed',e)
// console.error(err)
}
})
}
s.orphanedVideoCheck = function(monitor,checkMax,callback,forceCheck){
var finish = function(orphanedFilesCount){
if(callback)callback(orphanedFilesCount)
}
if(forceCheck === true || config.insertOrphans === true){
if(!checkMax){
checkMax = config.orphanedVideoCheckMax || 2
}
var videosDirectory = s.getVideoDirectory(monitor)// + s.formattedTime(video.time) + '.' + video.ext
fs.readdir(videosDirectory,function(err,files){
if(files && files.length > 0){
var fiveRecentFiles = files.slice(files.length - checkMax,files.length)
var completedFile = 0
var orphanedFilesCount = 0
var fileComplete = function(){
++completedFile
if(fiveRecentFiles.length === completedFile){
finish(orphanedFilesCount)
}
}
fiveRecentFiles.forEach(function(filename){
if(/T[0-9][0-9]-[0-9][0-9]-[0-9][0-9]./.test(filename)){
fs.stat(videosDirectory + filename,(err,stats) => {
if(!err && stats.size > 10){
s.knexQuery({
action: "select",
columns: "*",
table: "Videos",
where: [
['ke','=',monitor.ke],
['mid','=',monitor.mid],
['time','=',s.nameToTime(filename)],
],
limit: 1
},(err,rows) => {
if(!err && (!rows || !rows[0])){
++orphanedFilesCount
var video = rows[0]
s.insertCompletedVideo(monitor,{
file : filename
},() => {
fileComplete()
})
}else{
fileComplete()
}
})
}
})
}
})
}else{
finish()
}
})
}else{
finish()
}
}
s.streamMp4FileOverHttp = function(filePath,req,res,pureStream){
var ext = filePath.split('.')
ext = ext[ext.length - 1]
var total = fs.statSync(filePath).size;
if (req.headers['range'] && !pureStream) {
try{
var range = req.headers.range;
var parts = range.replace(/bytes=/, "").split("-");
var partialstart = parts[0];
var partialend = parts[1];
var start = parseInt(partialstart, 10);
var end = partialend ? parseInt(partialend, 10) : total-1;
var chunksize = (end-start)+1;
var file = fs.createReadStream(filePath, {start: start, end: end});
req.headerWrite={ 'Content-Range': 'bytes ' + start + '-' + end + '/' + total, 'Accept-Ranges': 'bytes', 'Content-Length': chunksize, 'Content-Type': 'video/'+ext }
req.writeCode=206
}catch(err){
req.headerWrite={ 'Content-Length': total, 'Content-Type': 'video/'+ext};
var file = fs.createReadStream(filePath)
req.writeCode=200
}
} else {
req.headerWrite={ 'Content-Length': total, 'Content-Type': 'video/'+ext};
var file = fs.createReadStream(filePath)
req.writeCode=200
}
if(req.query.downloadName){
req.headerWrite['content-disposition']='attachment; filename="'+req.query.downloadName+'"';
}
res.writeHead(req.writeCode,req.headerWrite);
file.on('close',function(){
res.end()
})
file.pipe(res)
return file
}
s.createVideoFromTimelapse = function(timelapseFrames,framesPerSecond,callback){
framesPerSecond = parseInt(framesPerSecond)
if(!framesPerSecond || isNaN(framesPerSecond))framesPerSecond = 2
var frames = timelapseFrames.reverse()
var ke = frames[0].ke
var mid = frames[0].mid
var finalFileName = frames[0].filename.split('.')[0] + '_' + frames[frames.length - 1].filename.split('.')[0] + `-${framesPerSecond}fps`
var concatFiles = []
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(fileLocation)
if(frameNumber === 0){
createLocation = fileLocationMid
}
})
if(concatFiles.length > framesPerSecond){
var commandTempLocation = `${s.dir.streams}${ke}/${mid}/mergeJpegs_${finalFileName}.sh`
var finalMp4OutputLocation = `${s.dir.fileBin}${ke}/${mid}/${finalFileName}.mp4`
if(!s.group[ke].activeMonitors[mid].buildingTimelapseVideo){
if(!fs.existsSync(finalMp4OutputLocation)){
var currentFile = 0
var completionTimeout
var commandString = `ffmpeg -y -f image2pipe -vcodec mjpeg -r ${framesPerSecond} -analyzeduration 10 -i - -q:v 1 -c:v libx264 -r ${framesPerSecond} "${finalMp4OutputLocation}"`
fs.writeFileSync(commandTempLocation,commandString)
var videoBuildProcess = spawn('sh',[commandTempLocation])
videoBuildProcess.stderr.on('data',function(data){
// console.log(data.toString())
clearTimeout(completionTimeout)
completionTimeout = setTimeout(function(){
if(currentFile === concatFiles.length - 1){
videoBuildProcess.kill('SIGTERM')
}
},4000)
})
videoBuildProcess.on('exit',function(data){
var timeNow = new Date()
var fileStats = fs.statSync(finalMp4OutputLocation)
var details = {}
s.knexQuery({
action: "insert",
table: "Files",
insert: {
ke: ke,
mid: mid,
details: s.s(details),
name: finalFileName + '.mp4',
size: fileStats.size,
time: timeNow,
}
})
s.setDiskUsedForGroup(ke,fileStats.size / 1048576,'fileBin')
fs.unlink(commandTempLocation,function(){
})
s.purgeDiskForGroup(ke)
setTimeout(() => {
delete(s.group[ke].activeMonitors[mid].buildingTimelapseVideo)
},5000)
})
var readFile = function(){
var filePath = concatFiles[currentFile]
// console.log(filePath,currentFile,'/',concatFiles.length - 1)
fs.readFile(filePath,function(err,buffer){
if(!err)videoBuildProcess.stdin.write(buffer)
if(currentFile === concatFiles.length - 1){
//is last
}else{
setTimeout(function(){
++currentFile
readFile()
},1/framesPerSecond)
}
})
}
readFile()
s.group[ke].activeMonitors[mid].buildingTimelapseVideo = videoBuildProcess
callback({
ok: true,
fileExists: false,
fileLocation: finalMp4OutputLocation,
msg: lang['Started Building']
})
}else{
callback({
ok: false,
fileExists: true,
filename: finalFileName + '.mp4',
fileLocation: finalMp4OutputLocation,
msg: lang['Already exists']
})
}
}else{
callback({
ok: false,
fileExists: false,
fileLocation: finalMp4OutputLocation,
msg: lang.Building
})
}
}else{
callback({
ok: false,
fileExists: false,
msg: lang.notEnoughFramesText1
})
}
}
s.getVideoStorageIndex = function(video){
try{
const monitorId = video.id || video.mid
const details = s.parseJSON(video.details) || {}
let storageId = details.storageId
const activeMonitor = s.group[video.ke] && s.group[video.ke].activeMonitors[monitorId] ? s.group[video.ke].activeMonitors[monitorId] : null;
if(activeMonitor && activeMonitor.addStorageId)storageId = activeMonitor.addStorageId;
if(storageId){
return s.group[video.ke].addStorageUse[storageId]
}
}catch(err){
s.debugLog(err)
}
return null
}
}