Add JPEG-based Timelapse creation

- Currently can only retrieve data about Timelapse through API, GUI coming soon
+ Definition fixes
merge-requests/63/head
Moe 2019-03-21 14:30:50 -07:00
parent 040080fd0b
commit 00ab3046ff
7 changed files with 364 additions and 20 deletions

View File

@ -18,6 +18,7 @@ module.exports = function(s,config,lang){
"description": "This is the primary task of the monitor.",
"default": "stop",
"example": "",
"selector": "h_m",
"possible": [
{
"name": lang.Disabled,
@ -45,13 +46,13 @@ module.exports = function(s,config,lang){
"name": "mid",
"field": lang["Monitor ID"],
"description": "This is a non-changeable identifier for the monitor. You can duplicate a monitor by double clicking the Monitor ID and changing it.",
"example": s.gid(),
"example": s.gid()
},
{
"name": "name",
"field": lang.Name,
"description": "This is the human-readable display name for the monitor.",
"example": "Bunny",
"example": "Bunny"
},
{
"name": "detail=max_keep_days",
@ -1242,7 +1243,7 @@ module.exports = function(s,config,lang){
"possible": ""
},
]
},
},
"Recording": {
"id": "monSectionRecording",
"name": lang.Recording,
@ -1251,6 +1252,7 @@ module.exports = function(s,config,lang){
"input-mapping": "record",
"blockquote": lang.RecordingText,
"blockquoteClass": 'global_tip',
"section-class": 'h_m_input h_m_record h_m_idle',
"info": [
// {
// "name": "height",
@ -1707,6 +1709,111 @@ module.exports = function(s,config,lang){
},
]
},
"Timelapse": {
"name": lang['Timelapse'],
"id": "monSectionTimelapse",
"color": "red",
"isSection": true,
"input-mapping": "record_timelapse",
"info": [
{
"name": "detail=record_timelapse",
"field": lang.Enabled,
"description": "Create a JPEG based timelapse.",
"default": "0",
"example": "",
"fieldType": "select",
"selector": "h_rec_ti",
"possible": [
{
"name": "No",
"value": "0"
},
{
"name": "Yes",
"value": "1"
}
]
},
{
hidden: true,
"name": "detail=record_timelapse_mp4",
"field": lang.Enabled,
"description": "Create an MP4 file at the end of each day for the timelapse.",
"default": "0",
"example": "",
"fieldType": "select",
"possible": [
{
"name": "No",
"value": "0"
},
{
"name": "Yes",
"value": "1"
}
]
},
{
hidden: true,
"name": "detail=record_timelapse_fps",
"field": lang['Creation Interval'],
"description": "",
"default": "900",
"example": "",
"form-group-class": "h_rec_ti_input h_rec_ti_1",
"fieldType": "select",
"possible": [
{
"name": `15 ${lang.minutes}`,
"value": "900"
},
{
"name": `30 ${lang.minutes}`,
"value": "1800"
},
{
"name": `45 ${lang.minutes}`,
"value": "2700"
},
{
"name": `60 ${lang.minutes}`,
"value": "3600"
}
]
},
{
hidden: true,
"name": "detail=record_timelapse_scale_x",
"field": lang['Image Width'],
"description": "",
"default": "",
"example": "",
"form-group-class": "h_rec_ti_input h_rec_ti_1",
"possible": ""
},
{
hidden: true,
"name": "detail=record_timelapse_scale_y",
"field": lang['Image Height'],
"description": "",
"default": "",
"example": "",
"form-group-class": "h_rec_ti_input h_rec_ti_1",
"possible": ""
},
{
hidden: true,
"name": "detail=record_timelapse_vf",
"field": lang['Video Filter'],
"description": "",
"default": "",
"example": "",
"form-group-class": "h_rec_ti_input h_rec_ti_1",
"possible": ""
},
]
},
"Custom": {
"name": "Custom",
"color": "navy",
@ -1976,16 +2083,6 @@ module.exports = function(s,config,lang){
{
hidden: true,
"name": "detail=detector_send_video_length",
"field": lang['Notification Video Length'],
"description": "",
"default": "10",
"example": "",
"form-group-class": "h_det_input h_det_1",
"form-group-class-pre-layer": "h_rec_mtd_input h_rec_mtd_hot h_rec_mtd_sip",
"possible": ""
},
{
"name": "detail=detector_send_video_length",
"field": lang["Notification Video Length"],
"description": "In seconds. The length of the video that gets sent to your Notification service, like Email or Discord.",
"default": "10",
@ -2522,15 +2619,13 @@ module.exports = function(s,config,lang){
]
},
{
hidden: true,
"name": lang['Object Detection'],
"color": "orange",
id: "monSectionDetectorObject",
headerTitle: `${lang['Object Detection']} <small>${lang['Plugin']} : <b class="shinobi-detector_name"></b> <b class="shinobi-detector-invert">${lang['Not Connected']}</b><b class="shinobi-detector" style="display:none">${lang['Connected']}</b></small>`,
headerTitle: `${lang['Object Detection']} <small><b class="shinobi-detector_name"></b> <b class="shinobi-detector-invert">${lang['Not Connected']}</b><b class="shinobi-detector" style="display:none">${lang['Connected']}</b></small>`,
isFormGroupGroup: true,
isSection: true,
"section-pre-class": "h_det_input h_det_1",
"section-class": "shinobi-detector-opencv shinobi-detector-openalpr shinobi-detector-yolo shinobi-detector-dlib shinobi-detector_plug",
"section-class": "h_det_input h_det_1",
"info": [
{
"name": "detail=detector_use_detect_object",
@ -2723,7 +2818,7 @@ module.exports = function(s,config,lang){
hidden: true,
"name": lang['Traditional Recording'],
"color": "orange",
id: "monSectionLisencePlateDetector",
id: "monSectionDetectorTraditionalRecording",
isSection: true,
isAdvanced: true,
isFormGroupGroup: true,

View File

@ -389,6 +389,9 @@
"Detector Recording Complete": "Detector Recording Complete",
"Clear Recorder Process": "Clear Recorder Process",
"Logging": "Logging",
"Timelapse": "Timelapse",
"Creation Interval": "Creation Interval",
"Plugin": "Plugin",
"IdentityText1": "This is how the system will identify the data for this stream. You cannot change the <b>Monitor ID</b> once you have pressed save. If you want you can make the <b>Monitor ID</b> more human readable before you continue.",
"IdentityText2": "You can duplicate a monitor by modifying the <b>Monitor ID</b> then pressing save. You <b>cannot</b> use the ID of a monitor that already exists or it will save over that monitor's database information.",
"opencvCascadesText": "If you see nothing here then just download this package of <a href=\"https://cdn.shinobi.video/weights/cascades.zip\">cascades</a>. Drop them into <code>plugins/opencv/cascades</code> then press refresh <i class=\"fa fa-retweet\"></i>.",

View File

@ -905,6 +905,24 @@ module.exports = function(s,config,lang,onFinish){
x.pipe += ' -q:v 1 -an -c:v copy -f hls -tune zerolatency -g 1 -hls_time 2 -hls_list_size 3 -start_number 0 -live_start_index 3 -hls_allow_cache 0 -hls_flags +delete_segments+omit_endlist "'+e.sdir+'coProcessor.m3u8"'
}
}
ffmpeg.buildTimelapseOutput = function(e,x){
if(e.details.record_timelapse === '1'){
if(e.details.input_map_choices&&e.details.input_map_choices.record_timelapse){
//add input feed map
x.pipe += s.createFFmpegMap(e,e.details.input_map_choices.record_timelapse)
}
var flags = []
if(e.details.record_timelapse_fps && e.details.record_timelapse_fps !== ''){
flags.push('-r 1/' + e.details.record_timelapse_fps)
}else{
flags.push('-r 1/900') // 15 minutes
}
if(e.details.record_timelapse_vf && e.details.record_timelapse_vf !== '')flags.push('-vf ' + e.details.record_timelapse_vf)
if(e.details.record_timelapse_scale_x && e.details.record_timelapse_scale_x !== '' && e.details.record_timelapse_scale_y && e.details.record_timelapse_scale_y !== '')flags.push(`-s ${e.details.record_timelapse_scale_x}x${e.details.record_timelapse_scale_y}`)
// x.pipe+=` -strftime 1 ${flags.join(' ')} -an -q:v 1 "${e.dirTimelapse}%Y-%m-%d/%Y-%m-%dT%H-%M-%S.jpg"`
x.pipe+=` -f singlejpeg ${flags.join(' ')} -an -q:v 1 pipe:7`
}
}
ffmpeg.assembleMainPieces = function(e,x){
//create executeable FFMPEG command
x.ffmpegCommandString = x.loglevel+x.input_fps;
@ -962,6 +980,7 @@ module.exports = function(s,config,lang,onFinish){
ffmpeg.buildAudioDetector(e,x)
ffmpeg.buildMainDetector(e,x)
ffmpeg.buildCoProcessorFeed(e,x)
ffmpeg.buildTimelapseOutput(e,x)
s.onFfmpegCameraStringCreationExtensions.forEach(function(extender){
extender(e,x)
})

View File

@ -625,13 +625,15 @@ module.exports = function(s,config,lang){
}
s.createCameraFolders = function(e){
//set the recording directory
var monitorRecordingFolder
if(e.details && e.details.dir && e.details.dir !== '' && config.childNodes.mode !== 'child'){
//addStorage choice
e.dir=s.checkCorrectPathEnding(e.details.dir)+e.ke+'/';
if (!fs.existsSync(e.dir)){
fs.mkdirSync(e.dir);
}
e.dir=e.dir+e.id+'/';
monitorRecordingFolder = e.dir + e.id + ''
e.dir = monitorRecordingFolder + '/'
if (!fs.existsSync(e.dir)){
fs.mkdirSync(e.dir);
}
@ -645,6 +647,12 @@ module.exports = function(s,config,lang){
if (!fs.existsSync(e.dir)){
fs.mkdirSync(e.dir);
}
monitorRecordingFolder = s.dir.videos + e.ke + '/' + e.id
}
//
e.dirTimelapse = monitorRecordingFolder + '_timelapse/'
if (!fs.existsSync(e.dirTimelapse)){
fs.mkdirSync(e.dirTimelapse)
}
// exec('chmod -R 777 '+e.dir,function(err){
//
@ -896,6 +904,52 @@ module.exports = function(s,config,lang){
audioDetector.start()
s.group[e.ke].mon[e.id].spawn.stdio[6].pipe(audioDetector.streamDecoder)
}
if(e.details.record_timelapse === '1'){
s.group[e.ke].mon[e.id].spawn.stdio[7].on('data',function(data){
var fileStream = s.group[e.ke].mon[e.id].recordTimelapseWriter
if(!fileStream){
var currentDate = s.formattedTime(null,'YYYY-MM-DD')
var filename = s.formattedTime() + '.jpg'
var location = e.dirTimelapse + currentDate + '/'
if(!fs.existsSync(location)){
fs.mkdirSync(location)
}
fileStream = fs.createWriteStream(location + filename)
fileStream.on('close', function () {
s.group[e.ke].mon[e.id].recordTimelapseWriter = null
var fileStats = fs.statSync(location + filename)
var fileInfo = {}
if(e.details && e.details.dir && e.details.dir !== ''){
fileInfo.dir = e.details.dir
}
fileInfo.size = fileStats.size
s.sqlQuery('SELECT * FROM Timelapses WHERE ke=? AND mid=? AND date=?',[e.ke,e.id,currentDate],function(err,rows){
if(rows && rows[0]){
var row = rows[0]
var details = s.parseJSON(row.details)
details.files[filename] = fileInfo
row.size += fileStats.size
s.sqlQuery('UPDATE Timelapses SET details=?,size=? WHERE ke=? AND mid=? AND date=?',[s.s(details),row.size,e.ke,e.id,currentDate],function(){
})
}else{
var details = {
files: {}
}
details.files[filename] = fileInfo
s.sqlQuery('INSERT INTO Timelapses (ke,mid,details,date,size) VALUES (?,?,?,?,?)',[e.ke,e.id,s.s(details),currentDate,fileStats.size])
}
})
})
s.group[e.ke].mon[e.id].recordTimelapseWriter = fileStream
}
fileStream.write(data)
clearTimeout(s.group[e.ke].mon[e.id].recordTimelapseWriterTimeout)
s.group[e.ke].mon[e.id].recordTimelapseWriterTimeout = setTimeout(function(){
fileStream.end()
},900)
})
}
if(e.details.detector === '1' && e.coProcessor === false){
s.ocvTx({f:'init_monitor',id:e.id,ke:e.ke})
//frames from motion detect

View File

@ -46,8 +46,12 @@ module.exports = function(s,config){
}
return newQuery
}
s.getUnixDate = function(value){
newValue = new Date(value).valueOf()
return newValue
}
s.stringToSqlTime = function(value){
newValue = new Date(value.replace('T',' '))
newValue = s.getUnixDate(s.nameToTime(value))
return newValue
}
s.sqlQuery = function(query,values,onMoveOn,hideLog){
@ -57,6 +61,11 @@ module.exports = function(s,config){
var values = [];
}
if(!onMoveOn){onMoveOn=function(){}}
// if(s.databaseOptions.client === 'pg'){
// query = query
// .replace(/ NOT LIKE /g," NOT ILIKE ")
// .replace(/ LIKE /g," ILIKE ")
// }
var mergedQuery = s.mergeQueryValues(query,values)
s.debugLog('s.sqlQuery QUERY',mergedQuery)
if(!s.databaseEngine || !s.databaseEngine.raw){
@ -109,6 +118,10 @@ module.exports = function(s,config){
s.sqlQuery("CREATE TABLE IF NOT EXISTS `Schedules` (`ke` varchar(50) DEFAULT NULL,`name` text,`details` text,`start` varchar(10) DEFAULT NULL,`end` varchar(10) DEFAULT NULL,`enabled` int(1) NOT NULL DEFAULT '1')" + mySQLtail + ';',[],function(err){
if(err)console.error(err)
},true)
//add Schedules table, will remove in future
s.sqlQuery("CREATE TABLE IF NOT EXISTS `Timelapses` (`ke`varchar(50)NOT NULL,`mid`varchar(50)NOT NULL,`details`longtext,`date`date NOT NULL,`time`timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,`end`timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,`size`int(11)NOT NULL)" + mySQLtail + ';',[],function(err){
if(err)console.error(err)
},true)
//add Cloud Videos table, will remove in future
s.sqlQuery('CREATE TABLE IF NOT EXISTS `Cloud Videos` (`mid` varchar(50) NOT NULL,`ke` varchar(50) DEFAULT NULL,`href` text NOT NULL,`size` float DEFAULT NULL,`time` timestamp NULL DEFAULT NULL,`end` timestamp NULL DEFAULT NULL,`status` int(1) DEFAULT \'0\',`details` text)' + mySQLtail + ';',[],function(err){
if(err)console.error(err)

View File

@ -1094,6 +1094,165 @@ module.exports = function(s,config,lang,app,io){
},res,req);
});
/**
* API : Get Timelapse images
*/
app.get([
config.webPaths.apiPrefix+':auth/timelapse/:ke',
config.webPaths.apiPrefix+':auth/timelapse/:ke/:id',
], function (req,res){
res.setHeader('Content-Type', 'application/json');
s.auth(req.params,function(user){
var hasRestrictions = user.details.sub && user.details.allmonitors !== '1'
if(
user.permissions.watch_videos==="0" ||
hasRestrictions && (!user.details.video_view || user.details.video_view.indexOf(req.params.id)===-1)
){
res.end(s.prettyPrint([]))
return
}
req.sql='SELECT * FROM `Timelapses` WHERE ke=?';req.ar=[req.params.ke];
if(req.query.archived=='1'){
req.sql+=' AND details LIKE \'%"archived":"1"\''
}
if(!req.params.id){
if(user.details.sub&&user.details.monitors&&user.details.allmonitors!=='1'){
try{user.details.monitors=JSON.parse(user.details.monitors);}catch(er){}
req.or=[];
user.details.monitors.forEach(function(v,n){
req.or.push('mid=?');req.ar.push(v)
})
req.sql+=' AND ('+req.or.join(' OR ')+')'
}
}else{
if(!user.details.sub||user.details.allmonitors!=='0'||user.details.monitors.indexOf(req.params.id)>-1){
req.sql+=' and mid=?'
req.ar.push(req.params.id)
}else{
res.end('[]');
return;
}
}
if(req.query.start||req.query.end){
if(req.query.start && req.query.start !== ''){
req.query.start = s.stringToSqlTime(req.query.start)
}
if(req.query.end && req.query.end !== ''){
req.query.end = s.stringToSqlTime(req.query.end)
}
if(!req.query.startOperator||req.query.startOperator==''){
req.query.startOperator='>='
}
if(!req.query.endOperator||req.query.endOperator==''){
req.query.endOperator='<='
}
var endIsStartTo
var theEndParameter = '`end`'
if(req.query.endIsStartTo){
endIsStartTo = true
theEndParameter = '`time`'
}
switch(true){
case(req.query.start&&req.query.start!==''&&req.query.end&&req.query.end!==''):
req.sql+=' AND `time` '+req.query.startOperator+' ? AND '+theEndParameter+' '+req.query.endOperator+' ?'
req.ar.push(req.query.start)
req.ar.push(req.query.end)
break;
case(req.query.start&&req.query.start!==''):
req.sql+=' AND `time` '+req.query.startOperator+' ?'
req.ar.push(req.query.start)
break;
case(req.query.end&&req.query.end!==''):
req.sql+=' AND '+theEndParameter+' '+req.query.endOperator+' ?'
req.ar.push(req.query.end)
break;
}
}
req.sql+=' ORDER BY `time` DESC'
s.sqlQuery(req.sql,req.ar,function(err,r){
if(!r){
res.end(s.prettyPrint([]))
return
}
r.forEach(function(row){
row.details = s.parseJSON(row.details)
})
res.end(s.prettyPrint(r))
})
},res,req);
});
/**
* API : Get Timelapse images
*/
app.get([
config.webPaths.apiPrefix+':auth/timelapse/:ke/:id/:date',
config.webPaths.apiPrefix+':auth/timelapse/:ke/:id/:date/:filename',
], function (req,res){
res.setHeader('Content-Type', 'application/json');
s.auth(req.params,function(user){
var hasRestrictions = user.details.sub && user.details.allmonitors !== '1'
if(
user.permissions.watch_videos==="0" ||
hasRestrictions && (!user.details.video_view || user.details.video_view.indexOf(req.params.id)===-1)
){
res.end(s.prettyPrint([]))
return
}
req.sql='SELECT * FROM `Timelapses` WHERE ke=?';req.ar=[req.params.ke];
if(req.query.archived=='1'){
req.sql+=' AND details LIKE \'%"archived":"1"\''
}
if(!req.params.id){
if(user.details.sub&&user.details.monitors&&user.details.allmonitors!=='1'){
try{user.details.monitors=JSON.parse(user.details.monitors);}catch(er){}
req.or=[];
user.details.monitors.forEach(function(v,n){
req.or.push('mid=?');req.ar.push(v)
})
req.sql+=' AND ('+req.or.join(' OR ')+')'
}
}else{
if(!user.details.sub||user.details.allmonitors!=='0'||user.details.monitors.indexOf(req.params.id)>-1){
req.sql+=' and mid=?'
req.ar.push(req.params.id)
}else{
res.end('[]');
return;
}
}
req.sql+=' and date=?'
req.ar.push(req.params.date)
req.sql+=' ORDER BY `time` DESC'
s.sqlQuery(req.sql,req.ar,function(err,r){
if(!r || !r[0]){
res.end(s.prettyPrint([]))
return
}
var timelapse = r[0]
timelapse.details = s.parseJSON(timelapse.details)
if(req.params.filename){
var fileInfo = timelapse.details.files[req.params.filename]
if(fileInfo){
res.contentType('image/jpeg')
var fileLocation
var currentDate = req.params.date
if(fileInfo.dir){
fileLocation = `${s.checkCorrectPathEnding(fileInfo.dir)}`
}else{
fileLocation = `${s.dir.videos}`
}
fileLocation = `${fileLocation}${timelapse.ke}/${timelapse.mid}_timelapse/${currentDate}/${req.params.filename}`
res.on('finish',function(){res.end()})
fs.createReadStream(fileLocation).pipe(res)
}else{
res.end(s.prettyPrint({ok: false, msg: lang['File Not Exist']}))
}
}else{
res.end(s.prettyPrint(timelapse))
}
})
},res,req);
});
/**
* API : Get Events
*/
app.get([config.webPaths.apiPrefix+':auth/events/:ke',config.webPaths.apiPrefix+':auth/events/:ke/:id',config.webPaths.apiPrefix+':auth/events/:ke/:id/:limit',config.webPaths.apiPrefix+':auth/events/:ke/:id/:limit/:start',config.webPaths.apiPrefix+':auth/events/:ke/:id/:limit/:start/:end'], function (req,res){

View File

@ -42,3 +42,4 @@ form.modal-body{margin:0}
.form-group-group.grey{border-color:#777}
.form-group-group.grey > h4{background:#777;color:#fff}
.dark .form-group-group{background:#222}
.form-group-group:last-child {margin-bottom: 0}